File size: 6,713 Bytes
8d40657 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
"""Abstract node representing data gathered from the analysis."""
from enum import Enum
def make_safe_label(label):
"""Avoid name clashes with GraphViz reserved words such as 'graph'."""
unsafe_words = ("digraph", "graph", "cluster", "subgraph", "node")
out = label
for word in unsafe_words:
out = out.replace(word, "%sX" % word)
return out.replace(".", "__").replace("*", "")
class Flavor(Enum):
"""Flavor describes the kind of object a node represents."""
UNSPECIFIED = "---" # as it says on the tin
UNKNOWN = "???" # not determined by analysis (wildcard)
NAMESPACE = "namespace" # node representing a namespace
ATTRIBUTE = "attribute" # attr of something, but not known if class or func.
IMPORTEDITEM = "import" # imported item of unanalyzed type
MODULE = "module"
CLASS = "class"
FUNCTION = "function"
METHOD = "method" # instance method
STATICMETHOD = "staticmethod"
CLASSMETHOD = "classmethod"
NAME = "name" # Python name (e.g. "x" in "x = 42")
# Flavors have a partial ordering in specificness of the information.
#
# This sort key scores higher on flavors that are more specific,
# allowing selective overwriting (while defining the override rules
# here, where that information belongs).
#
@staticmethod
def specificity(flavor):
if flavor in (Flavor.UNSPECIFIED, Flavor.UNKNOWN):
return 0
elif flavor in (Flavor.NAMESPACE, Flavor.ATTRIBUTE):
return 1
elif flavor == Flavor.IMPORTEDITEM:
return 2
else:
return 3
def __repr__(self):
return self.value
class Node:
"""A node is an object in the call graph.
Nodes have names, and reside in namespaces.
The namespace is a dot-delimited string of names. It can be blank, '',
denoting the top level.
The fully qualified name of a node is its namespace, a dot, and its name;
except at the top level, where the leading dot is omitted.
If the namespace has the special value None, it is rendered as *, and the
node is considered as an unknown node. A uses edge to an unknown node is
created when the analysis cannot determine which actual node is being used.
A graph node can be associated with an AST node from the analysis.
This identifies the syntax object the node represents, and as a bonus,
provides the line number at which the syntax object appears in the
analyzed code. The filename, however, must be given manually.
Nodes can also represent namespaces. These namespace nodes do not have an
associated AST node. For a namespace node, the "namespace" argument is the
**parent** namespace, and the "name" argument is the (last component of
the) name of the namespace itself. For example,
Node("mymodule", "main", None)
represents the namespace "mymodule.main".
Flavor describes the kind of object the node represents.
See the Flavor enum for currently supported values.
"""
def __init__(self, namespace, name, ast_node, filename, flavor):
self.namespace = namespace
self.name = name
self.ast_node = ast_node
self.filename = filename
self.flavor = flavor
self.defined = namespace is None # assume that unknown nodes are defined
def get_short_name(self):
"""Return the short name (i.e. excluding the namespace), of this Node.
Names of unknown nodes will include the *. prefix."""
if self.namespace is None:
return "*." + self.name
else:
return self.name
def get_annotated_name(self):
"""Return the short name, plus module and line number of definition site, if available.
Names of unknown nodes will include the *. prefix."""
if self.namespace is None:
return "*." + self.name
else:
if self.get_level() >= 1 and self.ast_node is not None:
return "%s\\n(%s:%d)" % (self.name, self.filename, self.ast_node.lineno)
else:
return self.name
def get_long_annotated_name(self):
"""Return the short name, plus namespace, and module and line number of definition site, if available.
Names of unknown nodes will include the *. prefix."""
if self.namespace is None:
return "*." + self.name
else:
if self.get_level() >= 1:
if self.ast_node is not None:
return "%s\\n\\n(%s:%d,\\n%s in %s)" % (
self.name,
self.filename,
self.ast_node.lineno,
repr(self.flavor),
self.namespace,
)
else:
return "%s\\n\\n(%s in %s)" % (self.name, repr(self.flavor), self.namespace)
else:
return self.name
def get_name(self):
"""Return the full name of this node."""
if self.namespace == "":
return self.name
elif self.namespace is None:
return "*." + self.name
else:
return self.namespace + "." + self.name
def get_level(self):
"""Return the level of this node (in terms of nested namespaces).
The level is defined as the number of '.' in the namespace, plus one.
Top level is level 0.
"""
if self.namespace == "":
return 0
else:
return 1 + self.namespace.count(".")
def get_toplevel_namespace(self):
"""Return the name of the top-level namespace of this node, or "" if none."""
if self.namespace == "":
return ""
if self.namespace is None: # group all unknowns in one namespace, "*"
return "*"
idx = self.namespace.find(".")
if idx > -1:
return self.namespace[0:idx]
else:
return self.namespace
def get_label(self):
"""Return a label for this node, suitable for use in graph formats.
Unique nodes should have unique labels; and labels should not contain
problematic characters like dots or asterisks."""
return make_safe_label(self.get_name())
def get_namespace_label(self):
"""Return a label for the namespace of this node, suitable for use
in graph formats. Unique nodes should have unique labels; and labels
should not contain problematic characters like dots or asterisks."""
return make_safe_label(self.namespace)
def __repr__(self):
return "<Node %s:%s>" % (repr(self.flavor), self.get_name()) |