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 (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: // TODO: Figure out a clean way to map namespaces 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: 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: // TODO: Very simplistic here - we can be smarter foreach ($ast->children as $c) { $name = var_name($c); if (!empty($name)) { add_var_scope($current_scope, $name, ''); } } 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 ($name === false) { 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') { 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); } } 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($ret_type, $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 { $type = node_type($file, $namespace, $ast->children[0], $current_scope, $current_class); if (!empty($functions[$current_scope]['oret'])) { // The function has a return type declared if (!type_check($type, $functions[$current_scope]['oret'], $namespace)) { Log::err(Log::ETYPE, "return {$type} but {$functions[$current_scope]['name']}() is declared to return {$functions[$current_scope]['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[$current_scope]['ret'])) { foreach (explode('|', $type) as $t) { if (!empty($t) && strpos($functions[$current_scope]['ret'], $t) === false) { $functions[$current_scope]['ret'] = $functions[$current_scope]['ret'] . '|' . $type; } } $functions[$current_scope]['ret'] = trim($functions[$current_scope]['ret'], '|'); } else { if ($current_scope != 'global') { $functions[$current_scope]['ret'] = $type; } } } } } break; case \ast\AST_CLASS_CONST_DECL: case \ast\AST_PROP_DECL: // TODO 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; }
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 (!empty($ast->children[0]->children[1]) && $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, 'pc_called' => true, '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); $summary['methods']++; $current_function = $method; $current_scope = "{$current_class}::{$method}"; if ($method == '__construct') { $classes[$lc]['pc_called'] = false; } if ($method == '__invoke') { $classes[$lc]['type'] = merge_type($classes[$lc]['type'], 'callable'); } 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); } // Set the declarted type to the doc-comment type and add |null if the default value is null $classes[$lc]['properties'][$node->children[0]]['dtype'] = $dc['vars'][$i]['type'] . ($type === 'null' ? '|null' : ''); $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'] = merge_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) { bc_check($file, $ast); } break; case \ast\AST_STATIC_CALL: // Indicate whether a class calls its parent constructor $call = $ast->children[0]; if ($call->kind == \ast\AST_NAME) { $func_name = strtolower($call->children[0]); if ($func_name == 'parent') { $meth = strtolower($ast->children[1]); if ($meth == '__construct') { $classes[strtolower($current_class)]['pc_called'] = true; } } } break; case \ast\AST_RETURN: case \ast\AST_PRINT: case \ast\AST_ECHO: case \ast\AST_STATIC_CALL: case \ast\AST_METHOD_CALL: if ($bc_checks) { bc_check($file, $ast); } 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; $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; }