Source code for pedal.tifa.tifa_visitor


"""
Main TIFA visitor-based algorithm here.

TODO: JoinedStr
"""

import ast
# TODO: FileType, DayType, TimeType,
from pedal.core.commands import system_error
from pedal.tifa.tifa_core import TifaCore, TifaAnalysis
from pedal.types.new_types import (Type, AnyType, ImpossibleType, FunctionType, GeneratorType,
                                   IntType, FloatType, BoolType, TupleType,
                                   ListType, StrType, SetType, DictType,
                                   ClassType, InstanceType, BuiltinConstructorType, NumType, NoneType,
                                   LiteralValue, LiteralInt, LiteralFloat, LiteralStr, LiteralBool,
                                   TypeUnion, widen_type, widest_type)
from pedal.types.new_types import is_subtype, specify_subtype
from pedal.types.normalize import get_pedal_type_from_value
from pedal.types.builtin import get_builtin_name
from pedal.types.operations import (VALID_UNARYOP_TYPES, apply_binary_operation, apply_unary_operation)
from pedal.tifa.constants import TOOL_NAME
from pedal.tifa.contexts import NewPath, NewScope
from pedal.tifa.feedbacks import (action_after_return, return_outside_function,
                                  write_out_of_scope, unconnected_blocks,
                                  iteration_problem, not_a_function,
                                  initialization_problem, unused_variable,
                                  overwritten_variable, iterating_over_non_list,
                                  iterating_over_empty_list, incompatible_types,
                                  parameter_type_mismatch, read_out_of_scope,
                                  type_changes, unnecessary_second_branch,
                                  recursive_call, multiple_return_types,
                                  possible_initialization_problem,
                                  incorrect_arity, else_on_loop_body,
                                  module_not_found, nested_function_definition,
                                  unused_returned_value, invalid_indexing,
                                  attribute_type_change)
from pedal.utilities.system import IS_AT_LEAST_PYTHON_38, IS_SKULPT


[docs] class Tifa(TifaCore, ast.NodeVisitor): """ TIFA subclass for traversing an AST and finding common issues. You can instantiate this class, manipulate settings, and then process some code or AST. """
[docs] def process_code(self, code, filename=None, reset=True): """ Processes the AST of the given source code to generate a report. Args: code (str): The Python source code filename (str): The filename of the source code (defaults to the submissions' main file). reset (bool): Whether or not to reset the results from the previous analysis before running this one. Returns: Report: The successful or successful report object """ if reset or self.analysis is None: self.analysis = TifaAnalysis() filename = filename or (self.report.submission.main_file if self.report.submission else 'student.py') self.line_offset = self.report.submission.line_offsets.get(filename, 0) if self.report.submission else 0 # Attempt parsing - might fail! try: ast_tree = ast.parse(code, filename) except Exception as error: self.analysis.fail(error) system_error(TOOL_NAME, "Could not parse code: " + str(error), report=self.report) return self.analysis # Attempt processing code - might fail! try: self.process_ast(ast_tree) except Exception as error: self.analysis.fail(error) system_error(TOOL_NAME, message="Successfully parsed but could not " "process AST: " + str(error), report=self.report) # Return whatever we got return self.analysis
[docs] def process_ast(self, ast_tree): """ Given an AST, actually performs the type and flow analyses to return a report. Args: ast_tree (AST): The AST object """ self.reset() # Traverse every node self.visit(ast_tree) # Update analysis, finish out the current scope. self.analysis.variables = self.name_map self._finish_scope() # Collect top level variables self._collect_top_level_variables() return self.analysis
[docs] def visit(self, node): """ Process this node by calling its appropriate visit_* Args: node (AST): The node to visit Returns: Type: The type calculated during the visit. """ # Start processing the node self.node_chain.append(node) self.ast_id += 1 # Actions after return? if len(self.scope_chain) > 1: return_state = self.find_variable_scope("*return") if return_state.exists and return_state.in_scope: if return_state.state.set == "yes": self._issue(action_after_return(self.locate(), report=self.report)) # No? All good, let's enter the node self.final_node = node result = ast.NodeVisitor.visit(self, node) # If a node failed to return something, return the UNKNOWN TYPE if result is None: result = AnyType() self.analysis.node_types[node] = result # Pop the node out of the chain self.ast_id -= 1 self.node_chain.pop() return result
def _visit_nodes(self, nodes): """ Visit all the nodes in the given list. Args: nodes (list): A list of values, of which any AST nodes will be visited. """ for node in nodes: if isinstance(node, ast.AST): self.visit(node) def _visit_collection_loop(self, node): was_empty = False # Handle the iteration list iter_list = node.iter iter_list_name = None if isinstance(iter_list, ast.Name): iter_list_name = iter_list.id if iter_list_name == "___": self._issue(unconnected_blocks(self.locate(iter_list), report=self.report)) state = self.iterate_variable(iter_list_name, self.locate(iter_list)) iter_type = state.type else: iter_type = self.visit(iter_list) if iter_type.is_empty: # TODO: It should check if its ONLY ever iterating over an empty list. # For now, only reports if we are NOT in a function was_empty = True if len(self.scope_chain) == 1: self._issue(iterating_over_empty_list(self.locate(iter_list), iter_list_name)) iter_subtype = iter_type.iterate() if isinstance(iter_subtype, ImpossibleType): self._issue(iterating_over_non_list(self.locate(iter_list), iter_list_name, report=self.report)) # Handle the iteration variable self.assign_target(node.target, iter_subtype, store_with_read=True) # Check that the iteration list and variable are distinct if isinstance(node.target, ast.Name) and node.target.id == iter_list_name: self._issue(iteration_problem(self.locate(node.target), node.target.id)) return was_empty def break_starred(self, elements): leading, starred, trailing = [], [], [] all_elements = iter(elements) for elt in all_elements: if isinstance(elt, ast.Starred): starred.append(elt) break else: leading.append(elt) else: return leading, starred, trailing for elt in all_elements: trailing.append(elt) return leading, starred, trailing # TODO: Properly handle assignments with subscripts
[docs] def assign_target(self, target, target_type, operation=None, store_with_read=False): """ Assign the type to the target, handling all kinds of assignment statements, including Names, Tuples/Lists, Subscripts, and Attributes. Args: target (ast.AST): The target AST Node. target_type (Type): The new TIFA type. operation (None | ast.op): The AugAssign operation, if there is one store_with_read: Whether to store the variable as a read variable, or just a write variable. Returns: """ if isinstance(target, ast.Name): original_value_type = self.visit(target) if operation: new_target_type = apply_binary_operation(operation, original_value_type, target_type) if isinstance(new_target_type, ImpossibleType): self._issue(incompatible_types(self.locate(), operation, original_value_type, target_type, report=self.report)) else: new_target_type = target_type if store_with_read: self.store_read_variable(target.id, new_target_type) else: self.store_variable(target.id, new_target_type) elif isinstance(target, (ast.Tuple, ast.List)): original_type = self.visit(target) leading, starred, trailing = self.break_starred(target.elts) tt, ot = target_type.break_apart(), original_type.break_apart() for elt, elt_type, old_type in zip(leading, tt, ot): self.assign_target(elt, elt_type) if starred: # TODO: Handle starred node's type changes # if not is_subtype(target_type, old_type): # self._issue(type_changes(self.locate(), 'an element of NODE', old_type.singular_name, elt_type.singular_name)) self.assign_target(starred[0], target_type) for elt, elt_type, old_type in zip(trailing, tt, ot): # BUG: Any trailing elements will be incorrectly offset, so won't work with finite length stuff self.assign_target(elt, elt_type) elif isinstance(target, ast.Subscript): original_value_type = self.visit(target.value) origin = self.identify_caller(target.value) original_indexing_type = self.visit(target.slice) original_type = original_value_type.index(original_indexing_type) if operation: new_target_type = apply_binary_operation(operation, original_type, target_type) if isinstance(new_target_type, ImpossibleType): self._issue(incompatible_types(self.locate(), operation, original_type, target_type, report=self.report)) else: new_target_type = target_type original_type.set_index(original_indexing_type, new_target_type) if origin: origin_type = self.load_variable(origin) self.store_variable(origin, origin_type.type) elif isinstance(target, ast.Attribute): original_type = self.visit(target.value) origin = self.identify_caller(target.value) if operation: original_type_field = original_type.get_attr(target.attr) new_target_type = apply_binary_operation(operation, original_type_field, target_type) if isinstance(new_target_type, ImpossibleType): self._issue(incompatible_types(self.locate(), operation, original_type, target_type, report=self.report)) else: new_target_type = target_type assigned_type = original_type.add_attr(target.attr, new_target_type) if isinstance(assigned_type, ImpossibleType): self._issue(attribute_type_change(self.locate(), target.attr, original_type, new_target_type, report=self.report)) if origin: origin_type = self.load_variable(origin) if origin_type: self.store_variable(origin, origin_type.type)
[docs] def visit_AnnAssign(self, node): """ Args: node (ast.AnnAssign): Returns: """ # Name, Attribute, or SubScript target = node.target # Type annotation = node.annotation annotation_type = self.evaluate_type(annotation) was_class_attribute = False # Optional assigned value value = node.value # 0 or 1, with 1 indicating pure names (not expressions) simple = node.simple # If it's a class attribute, then build up the type! if simple: current_scope = self.scope_chain[0] if current_scope in self.class_scopes: self.class_scopes[current_scope].add_attr(target.id, annotation) was_class_attribute = True # TODO: Allow setting for optional type coercion, or throw error if value: self.visit(value) # self.assign_target(target, self.visit(value)) # Make local variable either way self.assign_target(target, annotation_type, store_with_read=was_class_attribute)
[docs] def visit_Assign(self, node): """ Simple assignment statement: __targets__ = __value__ Args: node (ast.Assign): An Assign node Returns: None """ # Handle value value_type = self.visit(node.value) # Apply value to all targets for target in node.targets: self.assign_target(target, value_type)
[docs] def visit_AugAssign(self, node): """ Args: node (ast.AugAssign): Returns: """ # Handle value right = self.visit(node.value) if isinstance(node.target, ast.Name): self.load_variable(self.identify_caller(node.target)) self.assign_target(node.target, right, node.op)
[docs] def visit_Attribute(self, node): """ Args: node: Returns: """ # Handle value value_type = self.visit(node.value) self.check_common_bad_lookups(value_type, node.attr, node.value) # Handle attr result = value_type.get_attr(node.attr) # Set up self if this was a function (because now it's a method!) # TODO: Handle static/class functions differently I guess? if isinstance(result, FunctionType): result = result.as_method(value_type) return result
[docs] def visit_BinOp(self, node): """ Args: node: Returns: """ # Handle left and right left = self.visit(node.left) right = self.visit(node.right) operation = node.op new_target_type = apply_binary_operation(operation, left, right) if isinstance(new_target_type, ImpossibleType): self._issue(incompatible_types(self.locate(), operation, left, right, report=self.report)) return new_target_type
[docs] def visit_Bool(self, node): """ Visit a constant boolean value. Args: node (ast.AST): The boolean value Node. Returns: Type: A Bool type. """ return BoolType()
[docs] def visit_BoolOp(self, node): """ Args: node: Returns: """ # Handle left and right values = [self.visit(value) for value in node.values] # Handle operation # Actually, the operations are always the same behavior! # Handle truthiness if self.get_setting('truthiness_returns_booleans'): return BoolType() elif not values: return ImpossibleType() # All same type? Then return the first one! elif all(is_subtype(values[0], other) for other in values[1:]): return values[0] # Oh no, type union is necessary! else: return TypeUnion(values)
def load_root_variable(self, node, position=None): root_name = self.identify_caller(node) if root_name is not None: variable = self.find_variable_scope(root_name) if variable.exists: return variable.state return None def visit_Call(self, node): # Handle func part function_type = self.visit(node.func) # TODO: Need to grab the actual type in some situations original_callee = self.load_root_variable(node) # Handle args arguments = [self.visit(arg) for arg in node.args] if node.args else [] keywords = [(kwarg.arg, self.visit(kwarg.value)) for kwarg in node.keywords] if node.keywords else {} # Check special common mistakes if isinstance(function_type, BuiltinConstructorType): constructor = function_type.definition return constructor(self, function_type, original_callee, arguments, keywords, self.locate()) if isinstance(function_type, FunctionType): # Test if we have called this definition before if function_type.definition not in self.definition_chain: self.definition_chain.append(function_type.definition) # Function invocation result = function_type.definition(self, function_type, original_callee, arguments, keywords, self.locate()) self.definition_chain.pop() return result else: self._issue(recursive_call(self.locate(), original_callee, report=self.report)) elif isinstance(function_type, ClassType): constructor = function_type.get_constructor().definition self.definition_chain.append(constructor) new_instance = constructor(self, constructor, original_callee, arguments, keywords, self.locate()) self.definition_chain.pop() if '__init__' in function_type.fields: initializer = function_type.fields['__init__'].as_method(new_instance) if isinstance(initializer, FunctionType): self.definition_chain.append(initializer) initializer.definition(self, initializer, new_instance, [new_instance] + arguments, keywords, self.locate()) self.definition_chain.pop() return new_instance elif isinstance(function_type, (IntType, FloatType, NumType, ListType, DictType, SetType, TupleType, InstanceType, StrType, BoolType, NoneType, LiteralValue)): if original_callee is None: self._issue(not_a_function(self.locate(), "a value", function_type, report=self.report)) else: self._issue(not_a_function(self.locate(), original_callee.name, function_type, report=self.report)) return ImpossibleType() return AnyType()
[docs] def visit_ClassDef(self, node): """ Args: node: """ class_name = node.name parents = [self.visit(base) for base in node.bases] new_class_type = ClassType(class_name, {}, parents) self.store_variable(class_name, new_class_type) definitions_scope = self.scope_chain[:] class_scope = NewScope(self, definitions_scope, class_type=new_class_type) # TODO: Handle metaclass... somehow? #self._visit_nodes(node.keywords) with class_scope: self._visit_nodes(node.body) new_class_type = self.apply_decorators(class_name, new_class_type, node.decorator_list) return new_class_type
def apply_decorators(self, new_name, new_type, decorators): for decorator in reversed(decorators): old_type = self.load_variable(new_name) decorator_type = self.visit(decorator) original_callee = self.load_root_variable(decorator) new_type = decorator_type.definition(self, decorator_type, original_callee, [new_type], {}, self.locate()) self.store_variable(new_name, new_type) return new_type
[docs] def visit_Compare(self, node): """ Args: node: Returns: """ # Handle left and right left = self.visit(node.left) comparators = [self.visit(compare) for compare in node.comparators] # Handle ops for op, right in zip(node.ops, comparators): if isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)): continue elif isinstance(op, (ast.Lt, ast.LtE, ast.GtE, ast.Gt)): if type(right) in left.orderable: continue elif isinstance(op, (ast.In, ast.NotIn)): if right.allows_membership(left): continue self._issue(incompatible_types(self.locate(), op, left, right, report=self.report)) return BoolType()
[docs] def visit_comprehension(self, node): """ Args: node: """ self.fill_in_location(node, self.node_chain[-2]) self._visit_collection_loop(node) # Handle ifs, unless they're blank (None in Skulpt :) if node.ifs: self.visit_statements(node.ifs)
def fill_in_location(self, node, source_node): node.lineno = source_node.lineno node.col_offset = source_node.col_offset if IS_AT_LEAST_PYTHON_38: node.end_lineno = source_node.end_lineno node.end_col_offset = source_node.end_col_offset else: node.end_lineno = source_node.lineno node.end_col_offset = source_node.col_offset
[docs] def visit_Dict(self, node): """ Three types of dictionaries - empty - uniform type - record TODO: Handle records appropriately """ items = [(self.visit(key), self.visit(value)) for key, value in zip(node.keys, node.values) if key is not None] # Unpack starred dictionaries for key, value in zip(node.keys, node.values): if key is None: value_type = self.visit(value) if isinstance(value_type, DictType): items.extend([ (k, v) for k, v in value_type.element_types ]) # Empty dictionary if not items: return DictType([]) # All literal keys if all(isinstance(k, LiteralValue) for k, _ in items): return DictType([(k, v) for k, v in items]) # Find if all matched type first_key = widest_type([key for key, value in items]) first_value = widest_type([value for key, value in items]) if first_key is not None and first_value is not None: return DictType([(first_key, first_value)]) else: # Resort to just matching the types? return DictType([(k, v) for k, v in items])
[docs] def visit_DictComp(self, node): """ Args: node: Returns: """ generator_scope = NewScope(self, self.scope_chain[:]) with generator_scope: self._visit_nodes(node.generators) keys = self.visit(node.key) values = self.visit(node.value) return DictType([(keys, values)])
[docs] def visit_Expr(self, node): """ Any expression being used as a statement. Args: node (AST): An Expr node Returns: """ value = self.visit(node.value) if isinstance(node.value, ast.Call) and not isinstance(value, NoneType): # TODO: Helper function to get name with title ("append method") if isinstance(node.value.func, ast.Name): call_type = 'function' else: call_type = 'method' name = self.identify_caller(node.value) self._issue(unused_returned_value(self.locate(), name, call_type, value))
[docs] def visit_For(self, node): """ Args: node: """ was_empty = self._visit_collection_loop(node) # Handle the bodies # if not was_empty: # this_path_id = self.path_chain[0] # non_empty_path = NewPath(self, this_path_id, "f") # with non_empty_path: self.visit_statements(node.body) self.visit_statements(node.orelse)
[docs] def visit_FunctionDef(self, node): """ Args: node: Returns: """ function_name = node.name position = self.locate() definitions_scope = self.scope_chain[:] definition = self.make_function(function_name, definitions_scope, position, node) function = FunctionType(function_name, definition=definition) self.store_variable(function_name, function) if len(self.node_chain) > 2: self._issue(nested_function_definition(self.locate(), function_name)) return self.apply_decorators(function_name, function, node.decorator_list)
def make_function(self, function_name, definitions_scope, position, node): is_lambda = function_name is None class SkipType: pass # Pull apart args into a coherent set of data PRIOR to execution args = node.args pos_parameters = args.posonlyargs + args.args if IS_AT_LEAST_PYTHON_38 and not IS_SKULPT else args.args none_padding = [SkipType] * (len(pos_parameters) - len(args.defaults)) pos_defaults = none_padding + [self.visit(d) for d in args.defaults] kwarg_parameter = args.kwarg vararg_parameter = args.vararg # TODO: Finish kwargs named_parameters = args.kwonlyargs named_defaults = [self.visit(d) if d is not None else AnyType() for d in args.kw_defaults] expected_returns = self.evaluate_type(node.returns) if not is_lambda and node.returns else None def definition(tifa, function, callee_name, arguments, named_arguments, call_position): function_scope = NewScope(self, definitions_scope) with function_scope: # Process arguments if len(arguments) + len(named_arguments) != len(pos_parameters) and not vararg_parameter: self._issue(incorrect_arity(self.locate(), function_name, len(pos_parameters), len(arguments), report=self.report)) for parameter, default, argument in zip(pos_parameters, pos_defaults, arguments): parameter_name = parameter.arg if parameter.annotation: annotation = self.evaluate_type(parameter.annotation) if is_subtype(argument, annotation): specify_subtype(annotation, argument) else: self._issue(parameter_type_mismatch(self.locate(), parameter_name, annotation, argument)) elif default is not SkipType: if is_subtype(argument, default): specify_subtype(default, argument) else: self._issue(parameter_type_mismatch(self.locate(), parameter_name, default, argument)) if argument is not None: argument_type = argument.clone_mutably() self.create_variable(parameter_name, argument_type, position) # Too many arguments if len(pos_parameters) < len(arguments): if vararg_parameter: the_rest = arguments[len(pos_parameters):] self.create_variable(vararg_parameter.arg, TupleType(the_rest), position) # Not enough arguments if len(pos_parameters) > len(arguments): for parameter in pos_parameters[len(arguments):]: if parameter.annotation: annotation = self.evaluate_type(parameter.annotation) else: annotation = AnyType() self.create_variable(parameter.arg, annotation, position) if is_lambda: return self.visit(node.body) self.visit_statements(node.body) return_state = self.find_variable_scope("*return") return_value = NoneType() # TODO: Figure out if we are not returning something when we should # If the pseudo variable exists, we load it and get its type if return_state.exists and return_state.in_scope: return_state = self.load_variable("*return", call_position) return_value = return_state.type if expected_returns: if not is_subtype(return_value, expected_returns): self._issue(multiple_return_types(return_state.position, expected_returns.singular_name, return_value.singular_name, report=self.report)) return return_value return definition def evaluate_type(self, node): string_literal = False if isinstance(node, ast.Str): string_literal = node.s elif isinstance(node, ast.Constant) and isinstance(node.value, str): string_literal = node.value if string_literal is not False: if self.get_setting('evaluate_string_literal_types'): # TODO: Handle string literal types properly! pass else: return LiteralStr(string_literal) elif isinstance(node, ast.List): if node.elts: return ListType(False, self.evaluate_type(node.elts[0])) else: return ListType(True) elif isinstance(node, ast.Set) and node.elts: return TypeUnion([self.evaluate_type(e) for e in node.elts]) elif isinstance(node, ast.Tuple) and node.elts: return TupleType([self.evaluate_type(e) for e in node.elts]) elif isinstance(node, ast.Dict): if node.keys and node.values: return DictType([(self.evaluate_type(k), self.evaluate_type(v)) for k, v in zip(node.keys, node.values)]) else: return DictType([]) else: evaluated = self.visit(node) return evaluated.as_type(self, self.locate(node))
[docs] def visit_GeneratorExp(self, node): """ Args: node: Returns: """ generator_scope = NewScope(self, self.scope_chain[:]) with generator_scope: self._visit_nodes(node.generators) return GeneratorType(False, self.visit(node.elt))
[docs] def visit_If(self, node): """ Args: node: """ # Visit the conditional self.visit(node.test) if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.Pass): self._issue(unnecessary_second_branch(self.locate())) elif len(node.body) == 1 and isinstance(node.body[0], ast.Pass): if node.orelse: self._issue(unnecessary_second_branch(self.locate())) # Visit the bodies this_path_id = self.path_chain[0] if_path = NewPath(self, this_path_id, "i") with if_path: for statement in node.body: self.visit(statement) else_path = NewPath(self, this_path_id, "e") with else_path: for statement in node.orelse: self.visit(statement) # TODO: Unconditional return # Combine two paths into one # Check for any names that are on the IF path self.merge_paths(this_path_id, if_path.id, else_path.id)
[docs] def visit_IfExp(self, node): """ Args: node: Returns: """ # Visit the conditional self.visit(node.test) # Visit the body body = self.visit(node.body) # Visit the orelse orelse = self.visit(node.orelse) if is_subtype(body, orelse): return body # TODO: Union type? return TypeUnion([body, orelse])
[docs] def visit_Import(self, node): """ Args: node: """ # Handle names for alias in node.names: asname = alias.asname or alias.name module_type = self.load_module(alias.name) self.store_variable(asname, module_type)
[docs] def visit_ImportFrom(self, node): """ Args: node: """ # Handle names for alias in node.names: if alias.name == '*': module_type = self.load_module(node.module) for field, value in module_type.fields.items(): self.store_read_variable(field, value) else: if node.module is None: asname = alias.asname or alias.name module_type = self.load_module(alias.name) else: module_name = node.module asname = alias.asname or alias.name module_type = self.load_module(module_name) name_type = module_type.get_attr(alias.name) self.store_variable(asname, name_type)
[docs] def visit_Lambda(self, node): """ Args: node: Returns: """ position = self.locate() definitions_scope = self.scope_chain[:] definition = self.make_function(None, definitions_scope, position, node) return FunctionType("*lambda", definition=definition)
[docs] def visit_List(self, node): """ Args: node: Returns: """ if not node.elts: return ListType(True) # All literal keys elements = [self.visit(v) for v in node.elts] if all(isinstance(v, LiteralValue) for v in elements): # TODO: Allow type union instead? return ListType(False, elements[0].promote()) # Find if all matched type first_value = widest_type(elements) if first_value is not None: return ListType(False, first_value) else: # Resort to just matching the types? return ListType(False, TypeUnion(elements))
[docs] def visit_ListComp(self, node): """ Args: node: Returns: """ # TODO: Handle comprehension scope generator_scope = NewScope(self, self.scope_chain[:]) with generator_scope: self._visit_nodes(node.generators) return ListType(False, self.visit(node.elt))
[docs] def visit_NameConstant(self, node): """ Args: node: Returns: """ value = node.value if isinstance(value, bool): return LiteralBool(value) else: return NoneType()
[docs] def visit_Name(self, node): """ Args: node: Returns: """ name = node.id if name == "___": self._issue(unconnected_blocks(self.locate())) if isinstance(node.ctx, ast.Load): if name == "True" or name == "False": return LiteralBool(name == "True") elif name == "None": return NoneType() else: variable = self.find_variable_scope(name) builtin = get_builtin_name(name) if not variable.exists and builtin: return builtin else: state = self.load_variable(name) return state.type else: variable = self.find_variable_scope(name) if variable.exists: return variable.state.type else: return AnyType()
[docs] def visit_Num(self, node): """ Args: node: Returns: """ return LiteralInt(node.n) if isinstance(node.n, int) else LiteralFloat(node.n)
[docs] def visit_Constant(self, node) -> Type: """ Handle new 3.8's Constant node """ return get_pedal_type_from_value(node.value, self.evaluate_type)
[docs] def visit_Return(self, node): """ Args: node: """ if len(self.scope_chain) == 1: self._issue(return_outside_function(self.locate(), report=self.report)) # TODO: Unconditional return inside loop if node.value is not None: self.return_variable(self.visit(node.value)) else: self.return_variable(NoneType())
def visit_Set(self, node): # Fun fact, it's impossible to make a literal empty set if not node.elts: return SetType(True) # All literal keys elements = [self.visit(v) for v in node.elts] if all(isinstance(v, LiteralValue) for v in elements): # TODO: Allow type union instead? return SetType(False, elements[0].promote()) # Find if all matched type first_value = widest_type(elements) if first_value is not None: return SetType(False, first_value) else: # Resort to just matching the types? return SetType(False, TypeUnion(elements))
[docs] def visit_SetComp(self, node): """ Args: node: Returns: """ # TODO: Handle comprehension scope generator_scope = NewScope(self, self.scope_chain[:]) with generator_scope: self._visit_nodes(node.generators) return SetType(False, self.visit(node.elt))
[docs] def visit_statements(self, nodes): """ Args: nodes: Returns: """ # TODO: Check for pass in the middle of a series of statement if any(isinstance(node, ast.Pass) for node in nodes): pass return [self.visit(statement) for statement in nodes]
[docs] def visit_Str(self, node): """ Args: node: Returns: """ return LiteralStr(node.s)
def visit_JoinedStr(self, node): values = [self.visit(expr) for expr in node.values] # The result will be all StrType return StrType(is_empty=all(isinstance(n, (StrType, LiteralStr)) and n.is_empty for n in values)) def visit_FormattedValue(self, node): value = self.visit(node.value) if isinstance(value, StrType): return value else: return StrType(is_empty=False)
[docs] def visit_Subscript(self, node): """ Args: node: Returns: """ # Handle value value_type = self.visit(node.value) slice_type = self.visit(node.slice) result = value_type.index(slice_type) if isinstance(result, ImpossibleType): self._issue(invalid_indexing(self.locate(), value_type, slice_type)) if isinstance(node.slice, ast.Slice): return value_type.shallow_clone() else: return result
[docs] def visit_Slice(self, node): """ Handles a slice by visiting its components; cannot return a value because the slice is always the same type as its value, which is not available on the Slice node itself. """ if node.lower is not None: self.visit(node.lower) if node.upper is not None: self.visit(node.upper) if node.step is not None: self.visit(node.step)
def visit_Index(self, node): return self.visit(node.value) def visit_ExtSlice(self, node): return TupleType([self.visit(d) for d in node.dims]) def visit_Starred(self, node): return self.visit(node.value).break_apart() def visit_Tuple(self, node) -> TupleType: # Fun fact, it's impossible to make a literal empty set if not node.elts: return TupleType(True) # All literal keys return TupleType([self.visit(v) for v in node.elts])
[docs] def visit_UnaryOp(self, node): """ Args: node: Returns: """ # Handle operand operand_type = self.visit(node.operand) return apply_unary_operation(node.op, operand_type)
[docs] def visit_While(self, node): """ Args: node: """ # Visit conditional self.visit(node.test) # Visit the bodies this_path_id = self.path_id # One path is that we never enter the body empty_path = NewPath(self, this_path_id, "e") with empty_path: pass # Another path is that we loop through the body and check the test again body_path = NewPath(self, this_path_id, "w") with body_path: for statement in node.body: self.visit(statement) # Revisit conditional self.visit(node.test) # If there's else bodies (WEIRD) then we should check them afterwards if node.orelse: self._issue(else_on_loop_body(self.locate())) for statement in node.orelse: self.visit(statement) # Combine two paths into one # Check for any names that are on the IF path self.merge_paths(this_path_id, body_path.id, empty_path.id)
[docs] def visit_With(self, node): """ Args: node: """ for item in node.items: type_value = self.visit(item.context_expr) if item.optional_vars is not None: self.assign_target(item.optional_vars, type_value) # Handle the bodies self.visit_statements(node.body)