Lichen

Annotated resolving.py

114:a818b7ef8f7b
2016-10-20 Paul Boddie Added some more test cases including one featuring a "broken" attribute chain.
paul@12 1
#!/usr/bin/env python
paul@12 2
paul@12 3
"""
paul@12 4
Name resolution.
paul@12 5
paul@12 6
Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>
paul@12 7
paul@12 8
This program is free software; you can redistribute it and/or modify it under
paul@12 9
the terms of the GNU General Public License as published by the Free Software
paul@12 10
Foundation; either version 3 of the License, or (at your option) any later
paul@12 11
version.
paul@12 12
paul@12 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@12 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@12 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@12 16
details.
paul@12 17
paul@12 18
You should have received a copy of the GNU General Public License along with
paul@12 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@12 20
"""
paul@12 21
paul@12 22
from common import init_item, predefined_constants
paul@12 23
from results import AccessRef, InstanceRef, InvocationRef, LocalNameRef, \
paul@12 24
                    NameRef, ResolvedNameRef
paul@12 25
from referencing import Reference
paul@12 26
import sys
paul@12 27
paul@12 28
class NameResolving:
paul@12 29
paul@12 30
    "Resolving names mix-in for inspected modules."
paul@12 31
paul@12 32
    # Post-inspection resolution activities.
paul@12 33
paul@12 34
    def resolve(self):
paul@12 35
paul@12 36
        "Resolve dependencies and complete definitions."
paul@12 37
paul@12 38
        self.resolve_class_bases()
paul@21 39
        self.check_special()
paul@12 40
        self.check_names_used()
paul@27 41
        self.check_invocations()
paul@12 42
        self.resolve_initialisers()
paul@12 43
        self.resolve_literals()
paul@12 44
paul@12 45
    def resolve_class_bases(self):
paul@12 46
paul@12 47
        "Resolve all class bases since some of them may have been deferred."
paul@12 48
paul@12 49
        for name, bases in self.classes.items():
paul@12 50
            resolved = []
paul@12 51
            bad = []
paul@12 52
paul@12 53
            for base in bases:
paul@27 54
                ref = self.importer.identify(base.get_origin())
paul@12 55
paul@12 56
                # Obtain the origin of the base class reference.
paul@12 57
paul@12 58
                if not ref or not ref.has_kind("<class>"):
paul@12 59
                    bad.append(base)
paul@12 60
                    break
paul@12 61
paul@12 62
                resolved.append(ref)
paul@12 63
paul@12 64
            if bad:
paul@72 65
                print >>sys.stderr, "Bases of class %s were not classes: %s" % (name, ", ".join(map(str, bad)))
paul@12 66
            else:
paul@12 67
                self.importer.classes[name] = self.classes[name] = resolved
paul@12 68
paul@12 69
    def check_special(self):
paul@12 70
paul@12 71
        "Check special names."
paul@12 72
paul@12 73
        for name, value in self.special.items():
paul@28 74
            self.special[name] = self.importer.identify(value.get_origin())
paul@12 75
paul@12 76
    def check_names_used(self):
paul@12 77
paul@27 78
        "Check the external names used by each scope."
paul@12 79
paul@27 80
        for key, ref in self.name_references.items():
paul@27 81
            path, name = key.rsplit(".", 1)
paul@27 82
            self.resolve_accesses(path, name, ref)
paul@12 83
paul@27 84
    def check_invocations(self):
paul@12 85
paul@27 86
        "Find invocations amongst module data and replace their results."
paul@12 87
paul@27 88
        # Find members and replace invocation results with values. This is
paul@27 89
        # effectively the same as is done for initialised names, but refers to
paul@27 90
        # any unchanging value after initialisation.
paul@12 91
paul@27 92
        for key, ref in self.objects.items():
paul@27 93
            if ref.has_kind("<invoke>"):
paul@27 94
                ref = self.convert_invocation(ref)
paul@27 95
                self.importer.objects[key] = self.objects[key] = ref
paul@12 96
paul@31 97
        # Convert function defaults, which are effectively extra members of the
paul@31 98
        # module, and function locals.
paul@12 99
paul@31 100
        for fname, parameters in self.function_defaults.items():
paul@27 101
            l = []
paul@27 102
            for pname, ref in parameters:
paul@27 103
                if ref.has_kind("<invoke>"):
paul@27 104
                    ref = self.convert_invocation(ref)
paul@27 105
                l.append((pname, ref))
paul@27 106
            self.function_defaults[fname] = l
paul@12 107
paul@31 108
        # Convert function locals referencing invocations.
paul@31 109
paul@31 110
        for fname, names in self.function_locals.items():
paul@31 111
            for name, ref in names.items():
paul@31 112
                if ref.has_kind("<invoke>"):
paul@31 113
                    ref = self.convert_invocation(ref)
paul@31 114
                    names[name] = ref
paul@31 115
paul@27 116
    def convert_invocation(self, ref):
paul@27 117
paul@27 118
        "Convert the given invocation 'ref', handling instantiation."
paul@27 119
paul@27 120
        ref = self.importer.identify(ref.get_origin())
paul@27 121
        return ref and ref.has_kind("<class>") and ref.instance_of() or Reference("<var>")
paul@12 122
paul@12 123
    def resolve_accesses(self, path, name, ref):
paul@12 124
paul@12 125
        """
paul@12 126
        Resolve any unresolved accesses in the function at the given 'path'
paul@12 127
        for the given 'name' corresponding to the indicated 'ref'. Note that
paul@12 128
        this mechanism cannot resolve things like inherited methods because
paul@12 129
        they are not recorded as program objects in their inherited locations.
paul@12 130
        """
paul@12 131
paul@12 132
        attr_accesses = self.global_attr_accesses.get(path)
paul@12 133
        all_attrnames = attr_accesses and attr_accesses.get(name)
paul@12 134
paul@12 135
        if not all_attrnames:
paul@12 136
            return
paul@12 137
paul@12 138
        # Insist on constant accessors.
paul@12 139
paul@12 140
        if not ref.has_kind(["<class>", "<module>"]):
paul@12 141
            return
paul@12 142
paul@12 143
        found_attrnames = set()
paul@12 144
paul@12 145
        for attrnames in all_attrnames:
paul@12 146
paul@12 147
            # Start with the resolved name, adding attributes.
paul@12 148
paul@12 149
            attrs = ref.get_path()
paul@12 150
            remaining = attrnames.split(".")
paul@12 151
            last_ref = ref
paul@12 152
paul@12 153
            # Add each component, testing for a constant object.
paul@12 154
paul@12 155
            while remaining:
paul@12 156
                attrname = remaining[0]
paul@12 157
                attrs.append(attrname)
paul@12 158
                del remaining[0]
paul@12 159
paul@12 160
                # Find any constant object reference.
paul@12 161
paul@12 162
                attr_ref = self.get_resolved_object(".".join(attrs))
paul@12 163
paul@12 164
                # Non-constant accessors terminate the traversal.
paul@12 165
paul@12 166
                if not attr_ref or not attr_ref.has_kind(["<class>", "<module>", "<function>"]):
paul@12 167
paul@12 168
                    # Provide the furthest constant accessor unless the final
paul@12 169
                    # access can be resolved.
paul@12 170
paul@12 171
                    if remaining:
paul@12 172
                        remaining.insert(0, attrs.pop())
paul@12 173
                    else:
paul@12 174
                        last_ref = attr_ref
paul@12 175
                    break
paul@12 176
paul@12 177
                # Follow any reference to a constant object.
paul@12 178
                # Where the given name refers to an object in another location,
paul@12 179
                # switch to the other location in order to be able to test its
paul@12 180
                # attributes.
paul@12 181
paul@12 182
                last_ref = attr_ref
paul@12 183
                attrs = attr_ref.get_path()
paul@12 184
paul@12 185
            # Record a constant accessor only if an object was found
paul@12 186
            # that is different from the namespace involved.
paul@12 187
paul@12 188
            if last_ref:
paul@12 189
                objpath = ".".join(attrs)
paul@12 190
                if objpath != path:
paul@12 191
paul@27 192
                    if last_ref.has_kind("<invoke>"):
paul@27 193
                        last_ref = self.convert_invocation(last_ref)
paul@27 194
paul@12 195
                    # Establish a constant access.
paul@12 196
paul@12 197
                    init_item(self.const_accesses, path, dict)
paul@12 198
                    self.const_accesses[path][(name, attrnames)] = (objpath, last_ref, ".".join(remaining))
paul@12 199
paul@12 200
                    if len(attrs) > 1:
paul@12 201
                        found_attrnames.add(attrs[1])
paul@12 202
paul@12 203
        # Remove any usage records for the name.
paul@12 204
paul@12 205
        if found_attrnames:
paul@12 206
paul@12 207
            # NOTE: Should be only one name version.
paul@12 208
paul@12 209
            versions = []
paul@12 210
            for version in self.attr_usage[path][name]:
paul@12 211
                new_usage = set()
paul@12 212
                for usage in version:
paul@12 213
                    if found_attrnames.intersection(usage):
paul@12 214
                        new_usage.add(tuple(set(usage).difference(found_attrnames)))
paul@12 215
                    else:
paul@12 216
                        new_usage.add(usage)
paul@12 217
                versions.append(new_usage)
paul@12 218
paul@12 219
            self.attr_usage[path][name] = versions
paul@12 220
paul@12 221
    def resolve_initialisers(self):
paul@12 222
paul@12 223
        "Resolve initialiser values for names."
paul@12 224
paul@12 225
        # Get the initialisers in each namespace.
paul@12 226
paul@12 227
        for path, name_initialisers in self.name_initialisers.items():
paul@12 228
            const_accesses = self.const_accesses.get(path)
paul@12 229
paul@12 230
            # Resolve values for each name in a scope.
paul@12 231
paul@12 232
            for name, values in name_initialisers.items():
paul@12 233
                if path == self.name:
paul@12 234
                    assigned_path = name
paul@12 235
                else:
paul@12 236
                    assigned_path = "%s.%s" % (path, name)
paul@12 237
paul@12 238
                initialised_names = {}
paul@12 239
                aliased_names = {}
paul@12 240
paul@12 241
                for i, name_ref in enumerate(values):
paul@12 242
paul@12 243
                    # Unwrap invocations.
paul@12 244
paul@12 245
                    if isinstance(name_ref, InvocationRef):
paul@12 246
                        invocation = True
paul@12 247
                        name_ref = name_ref.name_ref
paul@12 248
                    else:
paul@12 249
                        invocation = False
paul@12 250
paul@12 251
                    # Obtain a usable reference from names or constants.
paul@12 252
paul@12 253
                    if isinstance(name_ref, ResolvedNameRef):
paul@12 254
                        if not name_ref.reference():
paul@12 255
                            continue
paul@12 256
                        ref = name_ref.reference()
paul@12 257
paul@12 258
                    # Obtain a reference from instances.
paul@12 259
paul@12 260
                    elif isinstance(name_ref, InstanceRef):
paul@12 261
                        if not name_ref.reference():
paul@12 262
                            continue
paul@12 263
                        ref = name_ref.reference()
paul@12 264
paul@12 265
                    # Resolve accesses that employ constants.
paul@12 266
paul@12 267
                    elif isinstance(name_ref, AccessRef):
paul@12 268
                        ref = None
paul@12 269
paul@12 270
                        if const_accesses:
paul@12 271
                            resolved_access = const_accesses.get((name_ref.original_name, name_ref.attrnames))
paul@12 272
                            if resolved_access:
paul@12 273
                                objpath, ref, remaining_attrnames = resolved_access
paul@12 274
                                if remaining_attrnames:
paul@12 275
                                    ref = None
paul@12 276
paul@12 277
                        # Accesses that do not employ constants cannot be resolved,
paul@12 278
                        # but they may be resolvable later.
paul@12 279
paul@12 280
                        if not ref:
paul@12 281
                            if not invocation:
paul@12 282
                                aliased_names[i] = name_ref.original_name, name_ref.attrnames, name_ref.number
paul@12 283
                            continue
paul@12 284
paul@12 285
                    # Attempt to resolve a plain name reference.
paul@12 286
paul@12 287
                    elif isinstance(name_ref, LocalNameRef):
paul@12 288
                        key = "%s.%s" % (path, name_ref.name)
paul@12 289
                        origin = self.name_references.get(key)
paul@12 290
paul@12 291
                        # Accesses that do not refer to known static objects
paul@12 292
                        # cannot be resolved, but they may be resolvable later.
paul@12 293
paul@12 294
                        if not origin:
paul@12 295
                            if not invocation:
paul@12 296
                                aliased_names[i] = name_ref.name, None, name_ref.number
paul@12 297
                            continue
paul@12 298
paul@12 299
                        ref = self.get_resolved_object(origin)
paul@12 300
                        if not ref:
paul@12 301
                            continue
paul@12 302
paul@12 303
                    elif isinstance(name_ref, NameRef):
paul@12 304
                        key = "%s.%s" % (path, name_ref.name)
paul@12 305
                        origin = self.name_references.get(key)
paul@12 306
                        if not origin:
paul@12 307
                            continue
paul@12 308
paul@12 309
                        ref = self.get_resolved_object(origin)
paul@12 310
                        if not ref:
paul@12 311
                            continue
paul@12 312
paul@12 313
                    else:
paul@12 314
                        continue
paul@12 315
paul@22 316
                    # Resolve any hidden dependencies involving external objects
paul@22 317
                    # or unresolved names referring to globals or built-ins.
paul@22 318
paul@22 319
                    if ref.has_kind("<depends>"):
paul@27 320
                        ref = self.importer.identify(ref.get_origin())
paul@22 321
paul@12 322
                    # Convert class invocations to instances.
paul@12 323
paul@58 324
                    if ref and invocation:
paul@27 325
                        ref = self.convert_invocation(ref)
paul@12 326
paul@27 327
                    if ref and not ref.has_kind("<var>"):
paul@12 328
                        initialised_names[i] = ref
paul@12 329
paul@12 330
                if initialised_names:
paul@12 331
                    self.initialised_names[assigned_path] = initialised_names
paul@12 332
                if aliased_names:
paul@12 333
                    self.aliased_names[assigned_path] = aliased_names
paul@12 334
paul@12 335
    def resolve_literals(self):
paul@12 336
paul@12 337
        "Resolve constant value types."
paul@12 338
paul@12 339
        # Get the constants defined in each namespace.
paul@12 340
paul@12 341
        for path, constants in self.constants.items():
paul@12 342
            for constant, n in constants.items():
paul@12 343
                objpath = "%s.$c%d" % (path, n)
paul@12 344
                _constant, value_type = self.constant_values[objpath]
paul@12 345
                self.initialised_names[objpath] = {0 : Reference("<instance>", value_type)}
paul@12 346
paul@12 347
        # Get the literals defined in each namespace.
paul@12 348
paul@12 349
        for path, literals in self.literals.items():
paul@12 350
            for n in range(0, literals):
paul@12 351
                objpath = "%s.$C%d" % (path, n)
paul@12 352
                value_type = self.literal_types[objpath]
paul@12 353
                self.initialised_names[objpath] = {0 : Reference("<instance>", value_type)}
paul@12 354
paul@29 355
    # Object resolution.
paul@29 356
paul@29 357
    def get_resolved_object(self, path):
paul@29 358
paul@29 359
        """
paul@29 360
        Get the details of an object with the given 'path' within this module.
paul@29 361
        Where the object has not been resolved, None is returned. This differs
paul@29 362
        from the get_object method used elsewhere in that it does not return an
paul@29 363
        unresolved object reference.
paul@29 364
        """
paul@29 365
paul@29 366
        if self.objects.has_key(path):
paul@29 367
            ref = self.objects[path]
paul@29 368
            if ref.has_kind("<depends>"):
paul@29 369
                return None
paul@29 370
            else:
paul@29 371
                return ref
paul@29 372
        else:
paul@29 373
            return None
paul@29 374
paul@12 375
# vim: tabstop=4 expandtab shiftwidth=4