Exemple #1
0
function pass2($file, $namespace, $ast, $current_scope, $parent_node = null, $current_class = null, $current_function = null, $parent_scope = null) : string
{
    global $classes, $functions, $namespace_map, $scope, $tainted_by, $quick_mode;
    static $next_node = 1;
    $vars = [];
    $parent_kind = null;
    if ($parent_node instanceof \ast\Node) {
        $parent_kind = $parent_node->kind;
    }
    if ($ast instanceof \ast\Node) {
        // Infinite Recursion check
        if (empty($ast->id)) {
            $ast->id = $next_node++;
        }
        if (!empty($parent_node)) {
            if ($parent_node->kind != \ast\AST_STMT_LIST) {
                if (empty($ast->visited_from[$parent_node->id])) {
                    $ast->visited_from[$parent_node->id] = 1;
                } else {
                    return $namespace;
                }
            }
        }
        switch ($ast->kind) {
            case \ast\AST_NAMESPACE:
                $namespace = (string) $ast->children[0] . '\\';
                break;
            case \ast\AST_USE_TRAIT:
                // We load up the trais in AST_CLASS, this part is just for pretty error messages
                foreach ($ast->children[0]->children as $trait) {
                    $name = $trait->children[0];
                    $lname = strtolower($name);
                    if (!empty($namespace_map[T_CLASS][$file][$lname])) {
                        $name = $namespace_map[T_CLASS][$file][$lname];
                    } else {
                        if ($trait->flags & \ast\flags\NAME_NOT_FQ) {
                            $name = $namespace . $name;
                        }
                    }
                    if (empty($classes[strtolower($name)])) {
                        Log::err(Log::EUNDEF, "Undeclared trait {$trait->children[0]}", $file, $ast->lineno);
                    }
                }
                break;
            case \ast\AST_CLASS:
                $lname = strtolower($namespace . $ast->name);
                if (empty($classes[$lname])) {
                    dump_scope($scope);
                    Log::err(Log::EFATAL, "Can't find class {$namespace}{$ast->name} - aborting", $file, $ast->lineno);
                }
                $current_class = $classes[$lname];
                $traits = $classes[$lname]['traits'];
                // Copy the trait over into this class
                foreach ($traits as $trait) {
                    if (empty($classes[$trait])) {
                        continue;
                    }
                    // TODO: Implement the various trait aliasing mechanisms here
                    $classes[$lname]['properties'] = array_merge($classes[$lname]['properties'], $classes[$trait]['properties']);
                    $classes[$lname]['constants'] = array_merge($classes[$lname]['constants'], $classes[$trait]['constants']);
                    $classes[$lname]['methods'] = array_merge($classes[$lname]['methods'], $classes[$trait]['methods']);
                    // Need the scope as well
                    foreach ($classes[$trait]['methods'] as $k => $method) {
                        if (empty($scope["{$classes[$trait]['name']}::{$method['name']}"])) {
                            continue;
                        }
                        $cs = $namespace . $ast->name . '::' . $method['name'];
                        if (!array_key_exists($cs, $scope)) {
                            $scope[$cs] = [];
                        }
                        if (!array_key_exists('vars', $scope[$cs])) {
                            $scope[$cs]['vars'] = [];
                        }
                        $scope[$cs] = $scope["{$classes[$trait]['name']}::{$method['name']}"];
                        // And finally re-map $this to point to this class
                        $scope[$cs]['vars']['this']['type'] = $namespace . $ast->name;
                    }
                }
                break;
            case \ast\AST_FUNC_DECL:
                if (empty($functions[strtolower($namespace . $ast->name)])) {
                    Log::err(Log::EFATAL, "Can't find function {$namespace}{$ast->name} - aborting", $file, $ast->lineno);
                }
                $current_function = $functions[strtolower($namespace . $ast->name)];
                $parent_scope = $current_scope;
                $current_scope = $namespace . $ast->name;
                break;
            case \ast\AST_CLOSURE:
                $closure_name = '{closure ' . $ast->id . '}';
                $functions[$closure_name] = node_func($file, false, $ast, $closure_name, '');
                $current_function = $closure_name;
                $parent_scope = $current_scope;
                $current_scope = $closure_name;
                if (!empty($scope[$parent_scope]['vars']['this'])) {
                    // TODO: check for a static closure
                    add_var_scope($current_scope, 'this', $scope[$parent_scope]['vars']['this']['type']);
                }
                if (!empty($ast->children[1]) && $ast->children[1]->kind == \ast\AST_CLOSURE_USES) {
                    $uses = $ast->children[1];
                    foreach ($uses->children as $use) {
                        if ($use->kind != \ast\AST_CLOSURE_VAR) {
                            Log::err(Log::EVAR, "You can only have variables in a closure use() clause", $file, $ast->lineno);
                        } else {
                            $name = var_name($use->children[0]);
                            if ($use->flags & \ast\flags\PARAM_REF) {
                                if (empty($parent_scope) || empty($scope[$parent_scope]['vars']) || empty($scope[$parent_scope]['vars'][$name])) {
                                    add_var_scope($parent_scope, $name, '');
                                }
                                $scope[$current_scope]['vars'][$name] =& $scope[$parent_scope]['vars'][$name];
                            } else {
                                if (empty($parent_scope) || empty($scope[$parent_scope]['vars']) || empty($scope[$parent_scope]['vars'][$name])) {
                                    Log::err(Log::EVAR, "Variable \${$name} is not defined", $file, $ast->lineno);
                                } else {
                                    $scope[$current_scope]['vars'][$name] = $scope[$parent_scope]['vars'][$name];
                                }
                            }
                        }
                    }
                }
                break;
            case \ast\AST_METHOD:
                if (empty($current_class['methods'][strtolower($ast->name)])) {
                    Log::err(Log::EFATAL, "Can't find method {$current_class['name']}:{$ast->name} - aborting", $file, $ast->lineno);
                }
                $current_function = $current_class['methods'][strtolower($ast->name)];
                $parent_scope = $current_scope;
                $current_scope = $current_class['name'] . '::' . $ast->name;
                break;
            case \ast\AST_USE:
                break;
            case \ast\AST_FOREACH:
                // Not doing depth-first here, because we need to declare the vars for the body of the loop
                if ($ast->children[2] instanceof \ast\Node && $ast->children[2]->kind == \ast\AST_LIST) {
                    Log::err(Log::EFATAL, "Can't use list() as a key element - aborting", $file, $ast->lineno);
                }
                if ($ast->children[1]->kind == \ast\AST_LIST) {
                    add_var_scope($current_scope, var_name($ast->children[1]->children[0]), '', true);
                    add_var_scope($current_scope, var_name($ast->children[1]->children[1]), '', true);
                } else {
                    // value
                    add_var_scope($current_scope, var_name($ast->children[1]), '', true);
                    // key
                    if (!empty($ast->children[2])) {
                        add_var_scope($current_scope, var_name($ast->children[2]), '', true);
                    }
                }
                break;
            case \ast\AST_CATCH:
                $obj = var_name($ast->children[0]);
                $name = var_name($ast->children[1]);
                if (!empty($name)) {
                    add_var_scope($current_scope, $name, $obj, true);
                }
                break;
        }
        // Depth-First for everything else
        foreach ($ast->children as $child) {
            $namespace = pass2($file, $namespace, $child, $current_scope, $ast, $current_class, $current_function, $parent_scope);
        }
        switch ($ast->kind) {
            case \ast\AST_ASSIGN:
            case \ast\AST_ASSIGN_REF:
                if ($ast->children[0] instanceof \ast\Node && $ast->children[0]->kind == \ast\AST_LIST) {
                    // TODO: Very simplistic here - we can be smarter
                    $rtype = node_type($file, $namespace, $ast->children[1], $current_scope, $current_class, $taint);
                    $type = generics($rtype);
                    foreach ($ast->children[0]->children as $c) {
                        $name = var_name($c);
                        if (!empty($name)) {
                            add_var_scope($current_scope, $name, $type);
                        }
                    }
                    break;
                }
                var_assign($file, $namespace, $ast, $current_scope, $current_class, $vars);
                foreach ($vars as $k => $v) {
                    if (empty($v)) {
                        $v = ['type' => '', 'tainted' => false, 'tainted_by' => ''];
                    }
                    if (empty($v['type'])) {
                        $v['type'] = '';
                    }
                    if (strpos($k, '::') === false) {
                        $cs = $current_scope;
                    } else {
                        $cs = 'global';
                    }
                    // Put static properties in the global scope TODO: revisit
                    // Check if we are assigning something to $GLOBALS[key]
                    if ($k == 'GLOBALS' && $ast->children[0]->kind == \ast\AST_DIM) {
                        $temp = $ast;
                        $depth = 0;
                        while ($temp->children[0]->kind == \ast\AST_DIM) {
                            $depth++;
                            $temp = $temp->children[0];
                        }
                        // If the index is a simple scalar, set it in the global scope
                        if (!empty($temp->children[1]) && !$temp->children[1] instanceof \ast\Node) {
                            $cs = 'global';
                            $k = $temp->children[1];
                            if ($depth == 1) {
                                $taint = false;
                                $tainted_by = '';
                                $v['type'] = node_type($file, $namespace, $ast->children[1], $current_scope, $current_class, $taint);
                                $v['tainted'] = $taint;
                                $v['tainted_by'] = $tainted_by;
                            } else {
                                // This is a $GLOBALS['a']['b'] type of assignment
                                // TODO: track array content types
                                $v['type'] = 'array';
                                $v['tainted'] = false;
                                $v['tainted_by'] = '';
                            }
                        }
                    }
                    if ($k == 'GLOBALS') {
                        break;
                    }
                    add_var_scope($cs, $k, $v['type']);
                    $scope[$cs]['vars'][$k]['tainted'] = $v['tainted'];
                    $scope[$cs]['vars'][$k]['tainted_by'] = $v['tainted_by'];
                }
                break;
            case \ast\AST_LIST:
                break;
            case \ast\AST_GLOBAL:
                if (!array_key_exists($current_scope, $scope)) {
                    $scope[$current_scope] = [];
                }
                if (!array_key_exists('vars', $scope[$current_scope])) {
                    $scope[$current_scope]['vars'] = [];
                }
                $name = var_name($ast);
                if (empty($name)) {
                    break;
                }
                if (!array_key_exists($name, $scope['global']['vars'])) {
                    add_var_scope('global', $name, '');
                }
                $scope[$current_scope]['vars'][$name] =& $scope['global']['vars'][$name];
                break;
            case \ast\AST_FOREACH:
                // check the array, the key,value part was checked on in the non-DPS part above
                $type = node_type($file, $namespace, $ast->children[0], $current_scope, $current_class);
                if (type_scalar($type)) {
                    Log::err(Log::ETYPE, "{$type} passed to foreach instead of array", $file, $ast->lineno);
                }
                break;
            case \ast\AST_STATIC:
                $name = var_name($ast);
                $type = node_type($file, $namespace, $ast->children[1], $current_scope, $current_class, $taint);
                add_var_scope($current_scope, $name, $type);
                $scope[$current_scope]['vars'][$name]['tainted'] = $taint;
                $scope[$current_scope]['vars'][$name]['tainted_by'] = $tainted_by;
                break;
            case \ast\AST_PRINT:
            case \ast\AST_ECHO:
                $taint = false;
                $tainted_by = '';
                $type = node_type($file, $namespace, $ast->children[0], $current_scope, $current_class, $taint);
                if ($type == 'array' || strlen($type) > 2 && substr($type, -2) == '[]') {
                    Log::err(Log::ETYPE, "array to string conversion", $file, $ast->lineno);
                }
                if ($taint) {
                    if (empty($tainted_by)) {
                        Log::err(Log::ETAINT, "possibly tainted output.", $file, $ast->lineno);
                    } else {
                        Log::err(Log::ETAINT, "possibly tainted output. Data tainted at {$tainted_by}", $file, $ast->lineno);
                    }
                }
                break;
            case \ast\AST_VAR:
                if ($parent_kind == \ast\AST_STMT_LIST) {
                    Log::err(Log::ENOOP, "no-op variable", $file, $ast->lineno);
                }
                break;
            case \ast\AST_ARRAY:
                if ($parent_kind == \ast\AST_STMT_LIST) {
                    Log::err(Log::ENOOP, "no-op array", $file, $ast->lineno);
                }
                break;
            case \ast\AST_CONST:
                if ($parent_kind == \ast\AST_STMT_LIST) {
                    Log::err(Log::ENOOP, "no-op constant", $file, $ast->lineno);
                }
                break;
            case \ast\AST_CLOSURE:
                if ($parent_kind == \ast\AST_STMT_LIST) {
                    Log::err(Log::ENOOP, "no-op closure", $file, $ast->lineno);
                }
                break;
            case \ast\AST_RETURN:
                // Check if there is a return type on the current function
                if (!empty($current_function['oret'])) {
                    $ret = $ast->children[0];
                    if ($ret instanceof \ast\Node) {
                        #	if($ast->children[0]->kind == \ast\AST_ARRAY) $ret_type='array';
                        #	else $ret_type = node_type($file, $namespace, $ret, $current_scope, $current_class);
                        $ret_type = node_type($file, $namespace, $ret, $current_scope, $current_class);
                    } else {
                        $ret_type = type_map(gettype($ret));
                        // This is distinct from returning actual NULL which doesn't hit this else since it is an AST_CONST node
                        if ($ret_type == 'NULL') {
                            $ret_type = 'void';
                        }
                    }
                    $check_type = $current_function['oret'];
                    if (strpos("|{$check_type}|", '|self|') !== false) {
                        $check_type = preg_replace("/\\bself\\b/", $current_class['name'], $check_type);
                    }
                    if (strpos("|{$check_type}|", '|static|') !== false) {
                        $check_type = preg_replace("/\\bstatic\\b/", $current_class['name'], $check_type);
                    }
                    if (strpos("|{$check_type}|", '|\\$this|') !== false) {
                        $check_type = preg_replace("/\\b\$this\\b/", $current_class['name'], $check_type);
                    }
                    if (!type_check(all_types($ret_type), all_types($check_type), $namespace)) {
                        Log::err(Log::ETYPE, "return {$ret_type} but {$current_function['name']}() is declared to return {$current_function['oret']}", $file, $ast->lineno);
                    }
                } else {
                    $lcs = strtolower($current_scope);
                    $type = node_type($file, $namespace, $ast->children[0], $current_scope, $current_class);
                    if (!empty($functions[$lcs]['oret'])) {
                        // The function has a return type declared
                        if (!type_check(all_types($type), all_types($functions[$lcs]['oret']), $namespace)) {
                            Log::err(Log::ETYPE, "return {$type} but {$functions[$lcs]['name']}() is declared to return {$functions[$lcs]['oret']}", $file, $ast->lineno);
                        }
                    } else {
                        if (strpos($current_scope, '::') !== false) {
                            list($class_name, $method_name) = explode('::', $current_scope, 2);
                            $idx = find_method_class($class_name, $method_name);
                            if ($idx) {
                                $classes[$idx]['methods'][strtolower($method_name)]['ret'] = $type;
                            }
                        } else {
                            if (!empty($functions[$lcs]['ret'])) {
                                foreach (explode('|', $type) as $t) {
                                    if (!empty($t) && strpos($functions[$lcs]['ret'], $t) === false) {
                                        $functions[$lcs]['ret'] = $functions[$lcs]['ret'] . '|' . $type;
                                    }
                                }
                                $functions[$lcs]['ret'] = trim($functions[$lcs]['ret'], '|');
                            } else {
                                if ($current_scope != 'global') {
                                    $functions[$lcs]['ret'] = $type;
                                }
                            }
                        }
                    }
                }
                break;
            case \ast\AST_CLASS_CONST_DECL:
            case \ast\AST_PROP_DECL:
                break;
            case \ast\AST_CALL:
                $found = false;
                $call = $ast->children[0];
                if ($call->kind == \ast\AST_NAME) {
                    $func_name = $call->children[0];
                    $found = null;
                    if ($call->flags & \ast\flags\NAME_NOT_FQ) {
                        if (!empty($namespace_map[T_FUNCTION][$file][strtolower($namespace . $func_name)])) {
                            $cs = $namespace_map[T_FUNCTION][$file][strtolower($namespace . $func_name)];
                            $found = $functions[strtolower($cs)];
                        } else {
                            if (!empty($namespace_map[T_FUNCTION][$file][strtolower($func_name)])) {
                                $cs = $namespace_map[T_FUNCTION][$file][strtolower($func_name)];
                                $found = $functions[strtolower($cs)];
                            } else {
                                if (!empty($functions[strtolower($namespace . $func_name)])) {
                                    $cs = $namespace . $func_name;
                                    $found = $functions[strtolower($cs)];
                                } else {
                                    if (!empty($functions[strtolower($func_name)])) {
                                        $cs = $func_name;
                                        $found = $functions[strtolower($func_name)];
                                    }
                                }
                            }
                        }
                    } else {
                        if (!empty($functions[strtolower($func_name)])) {
                            $cs = $func_name;
                            $found = $functions[strtolower($func_name)];
                        }
                    }
                    if (!$found) {
                        Log::err(Log::EUNDEF, "call to undefined function {$func_name}()", $file, $ast->lineno);
                    } else {
                        // Ok, the function exists, but are we calling it correctly?
                        if ($found instanceof ReflectionType) {
                            echo "oops at {$file}:{$ast->lineno}\n";
                        }
                        // DEBUG
                        arg_check($file, $namespace, $ast, $func_name, $found, $current_scope, $current_class);
                        if ($found['file'] != 'internal') {
                            // re-check the function's ast with these args
                            if (!$quick_mode) {
                                pass2($found['file'], $found['namespace'], $found['ast'], $found['scope'], $ast, $current_class, $found, $parent_scope);
                            }
                        } else {
                            if (!$found['avail']) {
                                if (!$found) {
                                    Log::err(Log::EAVAIL, "function {$func_name}() is not compiled into this version of PHP", $file, $ast->lineno);
                                }
                            }
                        }
                    }
                } else {
                    if ($call->kind == \ast\AST_VAR) {
                        $name = var_name($call);
                        if ($name instanceof \ast\Node) {
                            // $$var() - Ugh..
                            // TODO - something brilliant here
                        } else {
                            // $var() - hopefully a closure, otherwise we don't know
                            if (array_key_exists($name, $scope[$current_scope]['vars'])) {
                                if (($pos = strpos($scope[$current_scope]['vars'][$name]['type'], '{closure ')) !== false) {
                                    $closure_id = (int) substr($scope[$current_scope]['vars'][$name]['type'], $pos + 9);
                                    $func_name = '{closure ' . $closure_id . '}';
                                    $found = $functions[$func_name];
                                    arg_check($file, $namespace, $ast, $func_name, $found, $current_scope, $current_class);
                                    if (!$quick_mode) {
                                        pass2($found['file'], $found['namespace'], $found['ast'], $found['scope'], $ast, $current_class, $found, $parent_scope);
                                    }
                                }
                            }
                        }
                    }
                }
                break;
            case \ast\AST_NEW:
                $class_name = find_class_name($file, $ast, $namespace, $current_class, $current_scope);
                if ($class_name) {
                    $method_name = '__construct';
                    // No type checking for PHP4-style constructors
                    $method = find_method($class_name, $method_name);
                    if ($method) {
                        // Found a constructor
                        arg_check($file, $namespace, $ast, $method_name, $method, $current_scope, $current_class, $class_name);
                        if ($method['file'] != 'internal') {
                            // re-check the function's ast with these args
                            if (!$quick_mode) {
                                pass2($method['file'], $method['namespace'], $method['ast'], $method['scope'], $ast, $classes[strtolower($class_name)], $method, $parent_scope);
                            }
                        }
                    }
                }
                break;
            case \ast\AST_STATIC_CALL:
                $static_call_ok = false;
                $class_name = find_class_name($file, $ast, $namespace, $current_class, $current_scope, $static_call_ok);
                if ($class_name) {
                    // The class is declared, but does it have the method?
                    $method_name = $ast->children[1];
                    $method = find_method($class_name, $method_name);
                    if (is_array($method) && array_key_exists('avail', $method) && !$method['avail']) {
                        Log::err(Log::EAVAIL, "method {$class_name}::{$method_name}() is not compiled into this version of PHP", $file, $ast->lineno);
                    }
                    if ($method === false) {
                        Log::err(Log::EUNDEF, "static call to undeclared method {$class_name}::{$method_name}()", $file, $ast->lineno);
                    } else {
                        if ($method != 'dynamic') {
                            // Was it declared static?
                            if (!($method['flags'] & \ast\flags\MODIFIER_STATIC)) {
                                if (!$static_call_ok) {
                                    Log::err(Log::ESTATIC, "static call to non-static method {$class_name}::{$method_name}() defined at {$method['file']}:{$method['lineno']}", $file, $ast->lineno);
                                }
                            }
                            arg_check($file, $namespace, $ast, $method_name, $method, $current_scope, $current_class, $class_name);
                            if ($method['file'] != 'internal') {
                                // re-check the function's ast with these args
                                if (!$quick_mode) {
                                    pass2($method['file'], $method['namespace'], $method['ast'], $method['scope'], $ast, $classes[strtolower($class_name)], $method, $parent_scope);
                                }
                            }
                        }
                    }
                }
                break;
            case \ast\AST_METHOD_CALL:
                $class_name = find_class_name($file, $ast, $namespace, $current_class, $current_scope);
                if ($class_name) {
                    $method_name = $ast->children[1];
                    $method = find_method($class_name, $method_name);
                    if ($method === false) {
                        Log::err(Log::EUNDEF, "call to undeclared method {$class_name}->{$method_name}()", $file, $ast->lineno);
                    } else {
                        if ($method != 'dynamic') {
                            if (array_key_exists('avail', $method) && !$method['avail']) {
                                Log::err(Log::EAVAIL, "method {$class_name}::{$method_name}() is not compiled into this version of PHP", $file, $ast->lineno);
                            }
                            arg_check($file, $namespace, $ast, $method_name, $method, $current_scope, $current_class, $class_name);
                            if ($method['file'] != 'internal') {
                                // re-check the function's ast with these args
                                if (!$quick_mode) {
                                    pass2($method['file'], $method['namespace'], $method['ast'], $method['scope'], $ast, $classes[strtolower($class_name)], $method, $parent_scope);
                                }
                            }
                        }
                    }
                }
                break;
        }
    } else {
        if ($parent_kind == \ast\AST_STMT_LIST) {
            if ($ast !== null) {
                Log::err(Log::ENOOP, "(line number not accurate) dangling expression: " . var_export($ast, true), $file, $parent_node->lineno);
            }
        }
    }
    return $namespace;
}
Exemple #2
0
function node_type($file, $namespace, $node, $current_scope, $current_class, &$taint = null, $check_var_exists = true)
{
    global $classes, $functions, $scope, $namespace_map, $internal_arginfo;
    if (!$node instanceof \ast\Node) {
        if ($node === null) {
            return '';
        }
        return type_map(gettype($node));
    } else {
        if ($node->kind == \ast\AST_ARRAY) {
            if (!empty($node->children) && $node->children[0] instanceof \ast\Node && $node->children[0]->kind == \ast\AST_ARRAY_ELEM) {
                // Check the first 5 (completely arbitrary) elements and assume the rest are the same type
                $etypes = [];
                for ($i = 0; $i < 5; $i++) {
                    if (empty($node->children[$i])) {
                        break;
                    }
                    if ($node->children[$i]->children[0] instanceof \ast\Node) {
                        $etypes[] = node_type($file, $namespace, $node->children[$i]->children[0], $current_scope, $current_class, $temp_taint);
                    } else {
                        $etypes[] = type_map(gettype($node->children[$i]->children[0]));
                    }
                }
                $types = array_unique($etypes);
                if (count($types) == 1 && !empty($types[0])) {
                    return mkgenerics($types[0]);
                }
            }
            return 'array';
        } else {
            if ($node->kind == \ast\AST_BINARY_OP || $node->kind == \ast\AST_GREATER || $node->kind == \ast\AST_GREATER_EQUAL) {
                if ($node->kind == \ast\AST_BINARY_OP) {
                    $node_flags = $node->flags;
                } else {
                    $node_flags = $node->kind;
                }
                $taint = var_taint_check($file, $node, $current_scope);
                switch ($node_flags) {
                    // Always a string from a concat
                    case \ast\flags\BINARY_CONCAT:
                        $temp_taint = false;
                        node_type($file, $namespace, $node->children[0], $current_scope, $current_class, $temp_taint);
                        if ($temp_taint) {
                            $taint = true;
                            return 'string';
                        }
                        node_type($file, $namespace, $node->children[1], $current_scope, $current_class, $temp_taint);
                        if ($temp_taint) {
                            $taint = true;
                        }
                        return 'string';
                        break;
                        // Boolean unless invalid operands
                    // Boolean unless invalid operands
                    case \ast\flags\BINARY_IS_IDENTICAL:
                    case \ast\flags\BINARY_IS_NOT_IDENTICAL:
                    case \ast\flags\BINARY_IS_EQUAL:
                    case \ast\flags\BINARY_IS_NOT_EQUAL:
                    case \ast\flags\BINARY_IS_SMALLER:
                    case \ast\flags\BINARY_IS_SMALLER_OR_EQUAL:
                    case \ast\AST_GREATER:
                    case \ast\AST_GREATER_EQUAL:
                        $temp = node_type($file, $namespace, $node->children[0], $current_scope, $current_class);
                        if (!$temp) {
                            $left = '';
                        } else {
                            $left = type_map($temp);
                        }
                        $temp = node_type($file, $namespace, $node->children[1], $current_scope, $current_class);
                        if (!$temp) {
                            $right = '';
                        } else {
                            $right = type_map($temp);
                        }
                        $taint = false;
                        // If we have generics and no non-generics on the left and the right is not array-like ...
                        if (!empty(generics($left)) && empty(nongenerics($left)) && !type_check($right, 'array')) {
                            Log::err(Log::ETYPE, "array to {$right} comparison", $file, $node->lineno);
                        } else {
                            // and the same for the right side
                            if (!empty(generics($right)) && empty(nongenerics($right)) && !type_check($left, 'array')) {
                                Log::err(Log::ETYPE, "{$left} to array comparison", $file, $node->lineno);
                            }
                        }
                        return 'bool';
                        break;
                        // Add is special because you can add arrays
                    // Add is special because you can add arrays
                    case \ast\flags\BINARY_ADD:
                        $temp = node_type($file, $namespace, $node->children[0], $current_scope, $current_class);
                        if (!$temp) {
                            $left = '';
                        } else {
                            $left = type_map($temp);
                        }
                        $temp = node_type($file, $namespace, $node->children[1], $current_scope, $current_class);
                        if (!$temp) {
                            $right = '';
                        } else {
                            $right = type_map($temp);
                        }
                        // fast-track common cases
                        if ($left == 'int' && $right == 'int') {
                            return 'int';
                        }
                        if (($left == 'int' || $left == 'float') && ($right == 'int' || $right == 'float')) {
                            return 'float';
                        }
                        $left_is_array = !empty(generics($left)) && empty(nongenerics($left));
                        $right_is_array = !empty(generics($right)) && empty(nongenerics($right));
                        if ($left_is_array && !type_check($right, 'array')) {
                            Log::err(Log::ETYPE, "invalid operator: left operand is array and right is not", $file, $node->lineno);
                            return '';
                        } else {
                            if ($right_is_array && !type_check($left, 'array')) {
                                Log::err(Log::ETYPE, "invalid operator: right operand is array and left is not", $file, $node->lineno);
                                return '';
                            } else {
                                if ($left_is_array || $right_is_array) {
                                    // If it is a '+' and we know one side is an array and the other is unknown, assume array
                                    return 'array';
                                }
                            }
                        }
                        return 'int|float';
                        $taint = false;
                        break;
                        // Everything else should be an int/float
                    // Everything else should be an int/float
                    default:
                        $temp = node_type($file, $namespace, $node->children[0], $current_scope, $current_class);
                        if (!$temp) {
                            $left = '';
                        } else {
                            $left = type_map($temp);
                        }
                        $temp = node_type($file, $namespace, $node->children[1], $current_scope, $current_class);
                        if (!$temp) {
                            $right = '';
                        } else {
                            $right = type_map($temp);
                        }
                        if ($left == 'array' || $right == 'array') {
                            Log::err(Log::ETYPE, "invalid array operator", $file, $node->lineno);
                            return '';
                        } else {
                            if ($left == 'int' && $right == 'int') {
                                return 'int';
                            } else {
                                if ($left == 'float' || $right == 'float') {
                                    return 'float';
                                }
                            }
                        }
                        return 'int|float';
                        $taint = false;
                        break;
                }
            } else {
                if ($node->kind == \ast\AST_CAST) {
                    $taint = var_taint_check($file, $node->children[0], $current_scope);
                    switch ($node->flags) {
                        case \ast\flags\TYPE_NULL:
                            return 'null';
                            break;
                        case \ast\flags\TYPE_BOOL:
                            $taint = false;
                            return 'bool';
                            break;
                        case \ast\flags\TYPE_LONG:
                            $taint = false;
                            return 'int';
                            break;
                        case \ast\flags\TYPE_DOUBLE:
                            $taint = false;
                            return 'float';
                            break;
                        case \ast\flags\TYPE_STRING:
                            return 'string';
                            break;
                        case \ast\flags\TYPE_ARRAY:
                            return 'array';
                            break;
                        case \ast\flags\TYPE_OBJECT:
                            return 'object';
                            break;
                        default:
                            Log::err(Log::EFATAL, "Unknown type (" . $node->flags . ") in cast");
                    }
                } else {
                    if ($node->kind == \ast\AST_NEW) {
                        $class_name = find_class_name($file, $node, $namespace, $current_class, $current_scope);
                        if ($class_name) {
                            return $classes[strtolower($class_name)]['type'];
                        }
                        return 'object';
                    } else {
                        if ($node->kind == \ast\AST_DIM) {
                            $taint = var_taint_check($file, $node->children[0], $current_scope);
                            $type = node_type($file, $namespace, $node->children[0], $current_scope, $current_class);
                            if (!empty($type)) {
                                $gen = generics($type);
                                if (empty($gen)) {
                                    if ($type !== 'null' && !type_check($type, 'string|ArrayAccess')) {
                                        // array offsets work on strings, unfortunately
                                        // Double check that any classes in the type don't have ArrayAccess
                                        $ok = false;
                                        foreach (explode('|', $type) as $t) {
                                            if (!empty($t) && !is_native_type($t)) {
                                                if (!empty($classes[strtolower($t)]['type'])) {
                                                    if (strpos('|' . $classes[strtolower($t)]['type'] . '|', '|ArrayAccess|') !== false) {
                                                        $ok = true;
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                        if (!$ok) {
                                            Log::err(Log::ETYPE, "Suspicious array access to {$type}", $file, $node->lineno);
                                        }
                                    }
                                    return '';
                                }
                            } else {
                                return '';
                            }
                            return $gen;
                        } else {
                            if ($node->kind == \ast\AST_VAR) {
                                return var_type($file, $node, $current_scope, $taint, $check_var_exists);
                            } else {
                                if ($node->kind == \ast\AST_ENCAPS_LIST) {
                                    foreach ($node->children as $encap) {
                                        if ($encap instanceof \ast\Node) {
                                            if (var_taint_check($file, $encap, $current_scope)) {
                                                $taint = true;
                                            }
                                        }
                                    }
                                    return "string";
                                } else {
                                    if ($node->kind == \ast\AST_CONST) {
                                        if ($node->children[0]->kind == \ast\AST_NAME) {
                                            if (defined($node->children[0]->children[0])) {
                                                return type_map(gettype(constant($node->children[0]->children[0])));
                                            } else {
                                                // Todo: user-defined constant
                                            }
                                        }
                                    } else {
                                        if ($node->kind == \ast\AST_CLASS_CONST) {
                                            if ($node->children[1] == 'class') {
                                                return 'string';
                                            }
                                            // class name fetch
                                            $class_name = find_class_name($file, $node, $namespace, $current_class, $current_scope);
                                            if (!$class_name) {
                                                return '';
                                            }
                                            $ltemp = strtolower($class_name);
                                            while ($ltemp && !array_key_exists($node->children[1], $classes[$ltemp]['constants'])) {
                                                $ltemp = strtolower($classes[$ltemp]['parent']);
                                                if (empty($classes[$ltemp])) {
                                                    return '';
                                                }
                                                // undeclared class - will be caught elsewhere
                                            }
                                            if (!$ltemp || !array_key_exists($node->children[1], $classes[$ltemp]['constants'])) {
                                                Log::err(Log::EUNDEF, "can't access undeclared constant {$class_name}::{$node->children[1]}", $file, $node->lineno);
                                                return '';
                                            }
                                            return $classes[$ltemp]['constants'][$node->children[1]]['type'];
                                        } else {
                                            if ($node->kind == \ast\AST_PROP) {
                                                if ($node->children[0]->kind == \ast\AST_VAR) {
                                                    $class_name = find_class_name($file, $node, $namespace, $current_class, $current_scope);
                                                    if ($class_name && !$node->children[1] instanceof \ast\Node) {
                                                        $ltemp = find_property($file, $node, $class_name, $node->children[1], $class_name, false);
                                                        if (empty($ltemp)) {
                                                            return '';
                                                        }
                                                        return $classes[$ltemp]['properties'][$node->children[1]]['type'];
                                                    }
                                                }
                                            } else {
                                                if ($node->kind == \ast\AST_STATIC_PROP) {
                                                    if ($node->children[0]->kind == \ast\AST_NAME) {
                                                        $class_name = qualified_name($file, $node->children[0], $namespace);
                                                        if ($class_name && !$node->children[1] instanceof \ast\Node) {
                                                            $ltemp = find_property($file, $node, $class_name, $node->children[1], $class_name, false);
                                                            if (empty($ltemp)) {
                                                                return '';
                                                            }
                                                            return $classes[$ltemp]['properties'][$node->children[1]]['type'];
                                                        }
                                                    }
                                                } else {
                                                    if ($node->kind == \ast\AST_CALL) {
                                                        if ($node->children[0]->kind == \ast\AST_NAME) {
                                                            $func_name = $node->children[0]->children[0];
                                                            if ($node->children[0]->flags & \ast\flags\NAME_NOT_FQ) {
                                                                $func = $namespace_map[T_FUNCTION][$file][strtolower($namespace . $func_name)] ?? $namespace_map[T_FUNCTION][$file][strtolower($func_name)] ?? $functions[strtolower($namespace . $func_name)] ?? $functions[strtolower($func_name)] ?? null;
                                                            } else {
                                                                $func = $functions[strtolower($func_name)] ?? null;
                                                            }
                                                            if ($func['file'] == 'internal' && empty($func['ret'])) {
                                                                if (!empty($internal_arginfo[$func_name])) {
                                                                    return $internal_arginfo[$func_name][0] ?? '';
                                                                }
                                                            } else {
                                                                return $func['ret'] ?? '';
                                                            }
                                                        } else {
                                                            // TODO: Handle $func() and other cases that get here
                                                        }
                                                    } else {
                                                        if ($node->kind == \ast\AST_STATIC_CALL) {
                                                            $class_name = find_class_name($file, $node, $namespace, $current_class, $current_scope);
                                                            $method = find_method($class_name, $node->children[1]);
                                                            if ($method) {
                                                                return $method['ret'] ?? '';
                                                            }
                                                        } else {
                                                            if ($node->kind == \ast\AST_METHOD_CALL) {
                                                                $class_name = find_class_name($file, $node, $namespace, $current_class, $current_scope);
                                                                if ($class_name) {
                                                                    $method_name = $node->children[1];
                                                                    $method = find_method($class_name, $method_name);
                                                                    if ($method === false) {
                                                                        Log::err(Log::EUNDEF, "call to undeclared method {$class_name}->{$method_name}()", $file, $node->lineno);
                                                                    } else {
                                                                        if ($method != 'dynamic') {
                                                                            return $method['ret'];
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    return '';
}