# HG changeset patch # User Paul Boddie # Date 1476723394 -7200 # Node ID 3fe37462e0f6abb74b6c32ec40b5d5084663b8f2 # Parent 0e0944f7d36cd9e24cf51bf7782d2bb1629ec9b9 Track attribute assignment in attribute usage in order to restrict accessors. Changed the function type to be separate from all other types, preventing special __fn__ and __args__ attribute shadowing. diff -r 0e0944f7d36c -r 3fe37462e0f6 branching.py --- a/branching.py Mon Oct 17 15:39:07 2016 +0200 +++ b/branching.py Mon Oct 17 18:56:34 2016 +0200 @@ -77,15 +77,17 @@ else: return [b for b in self.get_all_suppliers(name) if name in b.assignments] - def set_usage(self, name, attrname, invocation=False): + def set_usage(self, name, attrname, invocation=False, assignment=False): """ Record usage on the given 'name' of the attribute 'attrname', noting the - invocation of the attribute if 'invocation' is set to a true value. + invocation of the attribute if 'invocation' is set to a true value, or + noting the assignment of the attribute if 'assignment' is set to a true + value. """ if self.usage.has_key(name): - self.usage[name].add((attrname, invocation)) + self.usage[name].add((attrname, invocation, assignment)) def get_usage(self): @@ -483,12 +485,13 @@ return branch - def use_attribute(self, name, attrname, invocation=False): + def use_attribute(self, name, attrname, invocation=False, assignment=False): """ Indicate the use on the given 'name' of an attribute with the given 'attrname', optionally involving an invocation of the attribute if - 'invocation' is set to a true value. + 'invocation' is set to a true value, or involving an assignment of the + attribute if 'assignment' is set to a true value. Return all branches that support 'name'. """ @@ -499,7 +502,7 @@ if branches.has_key(name): for branch in branches[name]: - branch.set_usage(name, attrname, invocation) + branch.set_usage(name, attrname, invocation, assignment) return branches[name] else: return None diff -r 0e0944f7d36c -r 3fe37462e0f6 common.py --- a/common.py Mon Oct 17 15:39:07 2016 +0200 +++ b/common.py Mon Oct 17 18:56:34 2016 +0200 @@ -834,11 +834,22 @@ invoked = [] if usage: - for attrname, invocation in usage: + for attrname, invocation, assignment in usage: if invocation: invoked.append(attrname) return invoked +def get_assigned_attributes(usage): + + "Obtain assigned attribute from the given 'usage'." + + assigned = [] + if usage: + for attrname, invocation, assignment in usage: + if assignment: + assigned.append(attrname) + return assigned + # Useful data. predefined_constants = "False", "None", "NotImplemented", "True" diff -r 0e0944f7d36c -r 3fe37462e0f6 deducer.py --- a/deducer.py Mon Oct 17 15:39:07 2016 +0200 +++ b/deducer.py Mon Oct 17 18:56:34 2016 +0200 @@ -19,7 +19,8 @@ this program. If not, see . """ -from common import first, get_attrname_from_location, get_attrnames, \ +from common import first, get_assigned_attributes, \ + get_attrname_from_location, get_attrnames, \ get_invoked_attributes, get_name_path, init_item, \ sorted_output, CommonOutput from encoders import encode_attrnames, encode_access_location, \ @@ -708,7 +709,7 @@ for attrnames in all_attrnames: attrname = get_attrnames(attrnames)[-1] access_location = (location, None, attrnames, 0) - self.add_usage_term(access_location, ((attrname, False),)) + self.add_usage_term(access_location, ((attrname, False, False),)) def add_usage(self, assignments, path): @@ -985,10 +986,11 @@ "Identify the types that can support each attribute name." self._init_attr_type_index(self.attr_class_types, self.importer.all_class_attrs) - self._init_attr_type_index(self.attr_instance_types, self.importer.all_combined_attrs) + self._init_attr_type_index(self.attr_instance_types, self.importer.all_instance_attrs, True) + self._init_attr_type_index(self.attr_instance_types, self.importer.all_combined_attrs, False) self._init_attr_type_index(self.attr_module_types, self.importer.all_module_attrs) - def _init_attr_type_index(self, attr_types, attrs): + def _init_attr_type_index(self, attr_types, attrs, assignment=None): """ Initialise the 'attr_types' attribute-to-types mapping using the given @@ -997,8 +999,20 @@ for name, attrnames in attrs.items(): for attrname in attrnames: - init_item(attr_types, attrname, set) - attr_types[attrname].add(name) + + # Permit general access for certain kinds of object. + + if assignment is None: + init_item(attr_types, (attrname, False), set) + init_item(attr_types, (attrname, True), set) + attr_types[(attrname, False)].add(name) + attr_types[(attrname, True)].add(name) + + # Restrict attribute assignment for instances. + + else: + init_item(attr_types, (attrname, assignment), set) + attr_types[(attrname, assignment)].add(name) def get_class_types_for_usage(self, usage): @@ -1035,21 +1049,19 @@ if not usage: return attrs.keys() - attrnames = [] - for attrname, invocation in usage: - attrnames.append(attrname) - - types = [] - - # Obtain types supporting the first attribute name... - - for name in attr_types.get(attrnames[0]) or []: - + keys = [] + for attrname, invocation, assignment in usage: + keys.append((attrname, assignment)) + + # Obtain types supporting the first (attribute name, assignment) key... + + types = set(attr_types.get(keys[0]) or []) + + for key in keys[1:]: + # Record types that support all of the other attributes as well. - _attrnames = attrs[name] - if set(attrnames).issubset(_attrnames): - types.append(name) + types.intersection_update(attr_types.get(key) or []) return types @@ -1241,6 +1253,7 @@ """ unit_path, name, attrnames, version = location + have_assignments = get_assigned_attributes(usage) # Detect any initialised name for the location. @@ -1249,7 +1262,7 @@ if ref: (class_types, only_instance_types, module_types, _function_types, _var_types) = separate_types([ref]) - return class_types, only_instance_types, module_types, True, False + return class_types, only_instance_types, module_types, True, have_assignments # Retrieve the recorded types for the usage. @@ -1261,7 +1274,7 @@ # for names involved with attribute accesses. if not name: - return class_types, only_instance_types, module_types, False, False + return class_types, only_instance_types, module_types, False, have_assignments # Obtain references to known objects. @@ -1271,7 +1284,8 @@ self.constrain_types(path, class_types, only_instance_types, module_types) if constrained_specific: - return class_types, only_instance_types, module_types, constrained_specific, constrained_specific + return class_types, only_instance_types, module_types, constrained_specific, \ + constrained_specific or have_assignments # Constrain "self" references. @@ -1279,9 +1293,9 @@ t = self.constrain_self_reference(unit_path, class_types, only_instance_types) if t: class_types, only_instance_types, module_types, constrained = t - return class_types, only_instance_types, module_types, constrained, False - - return class_types, only_instance_types, module_types, False, False + return class_types, only_instance_types, module_types, constrained, have_assignments + + return class_types, only_instance_types, module_types, False, have_assignments def constrain_self_reference(self, unit_path, class_types, only_instance_types): @@ -1381,7 +1395,7 @@ else: self.init_definition_details(location) - self.record_types_for_usage(location, [(attrname, False)]) + self.record_types_for_usage(location, [(attrname, False, False)]) constrained = location in self.accessor_constrained and constrained @@ -1403,7 +1417,8 @@ invocations = get_invoked_attributes(usage) - self.record_reference_types(accessor_location, class_types, instance_types, module_types, constrained, constrained_specific, invocations) + self.record_reference_types(accessor_location, class_types, instance_types, + module_types, constrained, constrained_specific, invocations) def record_types_for_attribute(self, access_location, attrname): @@ -1425,7 +1440,7 @@ "Return class, instance-only and module types supporting 'attrname'." - usage = ((attrname, False),) + usage = ((attrname, False, False),) class_types = self.get_class_types_for_usage(usage) only_instance_types = set(self.get_instance_types_for_usage(usage)).difference(class_types) diff -r 0e0944f7d36c -r 3fe37462e0f6 encoders.py --- a/encoders.py Mon Oct 17 15:39:07 2016 +0200 +++ b/encoders.py Mon Oct 17 18:56:34 2016 +0200 @@ -41,8 +41,8 @@ all_attrnames = [] for t in usage: - attrname, invocation = t - all_attrnames.append("%s%s" % (attrname, invocation and "!" or "")) + attrname, invocation, assignment = t + all_attrnames.append("%s%s" % (attrname, invocation and "!" or assignment and "=" or "")) return ", ".join(all_attrnames) or "{}" def decode_usage(s): @@ -51,7 +51,7 @@ all_attrnames = set() for attrname_str in s.split(", "): - all_attrnames.add((attrname_str.rstrip("!"), attrname_str.endswith("!"))) + all_attrnames.add((attrname_str.rstrip("!="), attrname_str.endswith("!"), attrname_str.endswith("="))) all_attrnames = list(all_attrnames) all_attrnames.sort() diff -r 0e0944f7d36c -r 3fe37462e0f6 inspector.py --- a/inspector.py Mon Oct 17 15:39:07 2016 +0200 +++ b/inspector.py Mon Oct 17 18:56:34 2016 +0200 @@ -41,6 +41,7 @@ BasicModule.__init__(self, name, importer) + self.in_assignment = False self.in_class = False self.in_conditional = False self.in_invocation = False @@ -342,7 +343,11 @@ elif isinstance(n, compiler.ast.AssAttr): if expr: self.process_structure_node(expr) + + in_assignment = self.in_assignment + self.in_assignment = True self.process_attribute_access(n) + self.in_assignment = in_assignment # Lists and tuples are matched against the expression and their # items assigned to expression items. @@ -362,12 +367,19 @@ "Process the given attribute access node 'n'." - # Obtain any completed chain and return the reference to it. + # Parts of the attribute chain are neither invoked nor assigned. in_invocation = self.in_invocation self.in_invocation = False + in_assignment = self.in_assignment + self.in_assignment = False + + # Obtain any completed chain and return the reference to it. + name_ref = self.process_attribute_chain(n) + self.in_invocation = in_invocation + self.in_assignment = in_assignment if self.have_access_expression(n): return name_ref @@ -423,8 +435,6 @@ immediate_access = len(self.attrs) == 1 assignment = immediate_access and isinstance(n, compiler.ast.AssAttr) - del self.attrs[0] - # Record global-based chains for subsequent resolution. is_global = self.in_function and not self.function_locals[path].has_key(name) or \ @@ -445,7 +455,8 @@ # Record attribute usage in the tracker, and record the branch # information for the access. - branches = tracker.use_attribute(name, attrname, self.in_invocation) + branches = tracker.use_attribute(name, attrname, self.in_invocation, + self.in_assignment and immediate_access) if not branches: raise InspectError("Name %s is accessed using %s before an assignment." % ( @@ -453,6 +464,8 @@ self.record_branches_for_access(branches, name, attrnames) access_number = self.record_access_details(name, attrnames, assignment) + + del self.attrs[0] return AccessRef(name, attrnames, access_number) def process_class_node(self, n): @@ -486,10 +499,13 @@ bases.append(base_class) # Record bases for the class and retain the class name. + # Note that the function class does not inherit from the object class. class_name = self.get_object_path(n.name) - if not bases and class_name != "__builtins__.core.object": + if not bases and class_name != "__builtins__.core.object" and \ + class_name != "__builtins__.core.function": + ref = self.get_object("__builtins__.object") bases.append(ref) @@ -509,8 +525,14 @@ self.in_class = class_name self.set_instance_attr("__class__", Reference("", class_name)) self.enter_namespace(n.name) - self.set_name("__fn__") # special instantiator attribute - self.set_name("__args__") # special instantiator attribute + + # Do not provide the special instantiator attributes on the function + # class. Function instances provide these attributes. + + if class_name != "__builtins__.core.function": + self.set_name("__fn__") # special instantiator attribute + self.set_name("__args__") # special instantiator attribute + self.assign_general_local("__name__", self.get_constant("str", class_name)) self.process_structure_node(n.code) self.exit_namespace() @@ -710,10 +732,14 @@ self.allocate_arguments(path, n.args) try: - # Process the expression, obtaining any identified reference. + # Communicate to the invocation target expression that it forms the + # target of an invocation, potentially affecting attribute accesses. in_invocation = self.in_invocation self.in_invocation = True + + # Process the expression, obtaining any identified reference. + name_ref = self.process_structure_node(n.node) self.in_invocation = in_invocation diff -r 0e0944f7d36c -r 3fe37462e0f6 lib/__builtins__/core.py --- a/lib/__builtins__/core.py Mon Oct 17 15:39:07 2016 +0200 +++ b/lib/__builtins__/core.py Mon Oct 17 18:56:34 2016 +0200 @@ -3,7 +3,7 @@ """ Core objects. -Copyright (C) 2015 Paul Boddie +Copyright (C) 2015, 2016 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,22 +20,41 @@ """ class object: + + "The root class of all objects except functions." + def __init__(self): "No-operation." pass + def __bool__(self): "Objects are true by default." return True -class function(object): +class function: + + """ + The class of all function objects. + Note that as a special case, function does not inherit from object. + """ + def __init__(self): - # Reserve special attributes for function instances. + """ + Reserve special attributes for function instances. + """ self.__fn__ = None self.__args__ = None + def __bool__(self): + "Functions are true by default." + return True + class type(object): + + "The class of all classes." + pass class BaseException(object): pass diff -r 0e0944f7d36c -r 3fe37462e0f6 tests/attr_providers.py --- a/tests/attr_providers.py Mon Oct 17 15:39:07 2016 +0200 +++ b/tests/attr_providers.py Mon Oct 17 18:56:34 2016 +0200 @@ -16,6 +16,13 @@ def f(x): return x.a, x.b +def g(x): + + # Should only permit D instance and E. + + x.a = 7 + x.b = 8 + c = C() d = D() e = E()