| 1 | """
|
| 2 | format.py -- Pretty print an ASDL data structure.
|
| 3 |
|
| 4 | Like encode.py, but uses text instead of binary.
|
| 5 |
|
| 6 | TODO:
|
| 7 |
|
| 8 | - auto-abbreviation of single field things (minus location)
|
| 9 |
|
| 10 | - option to omit spaces for SQ, SQ, W? It's all one thing.
|
| 11 |
|
| 12 | Places where we try a single line:
|
| 13 | - arrays
|
| 14 | - objects with name fields
|
| 15 | - abbreviated, unnamed fields
|
| 16 | """
|
| 17 |
|
| 18 | import re
|
| 19 |
|
| 20 | from asdl import asdl_ as asdl
|
| 21 | from core import util
|
| 22 |
|
| 23 | import os
|
| 24 | if not os.getenv('_OVM_DEPS'):
|
| 25 | import cgi
|
| 26 |
|
| 27 |
|
| 28 | def DetectConsoleOutput(f):
|
| 29 | """Wrapped to auto-detect."""
|
| 30 | if f.isatty():
|
| 31 | return AnsiOutput(f)
|
| 32 | else:
|
| 33 | return TextOutput(f)
|
| 34 |
|
| 35 |
|
| 36 | class ColorOutput(object):
|
| 37 | """Abstract base class for plain text, ANSI color, and HTML color."""
|
| 38 |
|
| 39 | def __init__(self, f):
|
| 40 | self.f = f
|
| 41 | self.num_chars = 0
|
| 42 |
|
| 43 | def NewTempBuffer(self):
|
| 44 | """Return a temporary buffer for the line wrapping calculation."""
|
| 45 | raise NotImplementedError
|
| 46 |
|
| 47 | def FileHeader(self):
|
| 48 | """Hook for printing a full file."""
|
| 49 | pass
|
| 50 |
|
| 51 | def FileFooter(self):
|
| 52 | """Hook for printing a full file."""
|
| 53 | pass
|
| 54 |
|
| 55 | def PushColor(self, str_type):
|
| 56 | raise NotImplementedError
|
| 57 |
|
| 58 | def PopColor(self):
|
| 59 | raise NotImplementedError
|
| 60 |
|
| 61 | def write(self, s):
|
| 62 | self.f.write(s)
|
| 63 | self.num_chars += len(s) # Only count visible characters!
|
| 64 |
|
| 65 | def WriteRaw(self, raw):
|
| 66 | """
|
| 67 | Write raw data without escaping, and without counting control codes in the
|
| 68 | length.
|
| 69 | """
|
| 70 | s, num_chars = raw
|
| 71 | self.f.write(s)
|
| 72 | self.num_chars += num_chars
|
| 73 |
|
| 74 | def NumChars(self):
|
| 75 | return self.num_chars
|
| 76 |
|
| 77 | def GetRaw(self):
|
| 78 | # For when we have an io.StringIO()
|
| 79 | return self.f.getvalue(), self.num_chars
|
| 80 |
|
| 81 |
|
| 82 | class TextOutput(ColorOutput):
|
| 83 | """TextOutput put obeys the color interface, but outputs nothing."""
|
| 84 |
|
| 85 | def __init__(self, f):
|
| 86 | ColorOutput.__init__(self, f)
|
| 87 |
|
| 88 | def NewTempBuffer(self):
|
| 89 | return TextOutput(util.Buffer())
|
| 90 |
|
| 91 | def PushColor(self, str_type):
|
| 92 | pass # ignore color
|
| 93 |
|
| 94 | def PopColor(self):
|
| 95 | pass # ignore color
|
| 96 |
|
| 97 |
|
| 98 | class HtmlOutput(ColorOutput):
|
| 99 | """
|
| 100 | HTML one can have wider columns. Maybe not even fixed-width font. Hm yeah
|
| 101 | indentation should be logical then?
|
| 102 |
|
| 103 | Color: HTML spans
|
| 104 | """
|
| 105 | def __init__(self, f):
|
| 106 | ColorOutput.__init__(self, f)
|
| 107 |
|
| 108 | def NewTempBuffer(self):
|
| 109 | return HtmlOutput(util.Buffer())
|
| 110 |
|
| 111 | def FileHeader(self):
|
| 112 | # TODO: Use a different CSS file to make the colors match. I like string
|
| 113 | # literals as yellow, etc.
|
| 114 | #<link rel="stylesheet" type="text/css" href="/css/code.css" />
|
| 115 | self.f.write("""
|
| 116 | <html>
|
| 117 | <head>
|
| 118 | <title>oil AST</title>
|
| 119 | <style>
|
| 120 | .n { color: brown }
|
| 121 | .s { font-weight: bold }
|
| 122 | .o { color: darkgreen }
|
| 123 | </style>
|
| 124 | </head>
|
| 125 | <body>
|
| 126 | <pre>
|
| 127 | """)
|
| 128 |
|
| 129 | def FileFooter(self):
|
| 130 | self.f.write("""
|
| 131 | </pre>
|
| 132 | </body>
|
| 133 | </html>
|
| 134 | """)
|
| 135 |
|
| 136 | def PushColor(self, str_type):
|
| 137 | # To save bandwidth, use single character CSS names.
|
| 138 | if str_type == _NODE_TYPE:
|
| 139 | css_class = 'n'
|
| 140 | elif str_type == _STRING_LITERAL:
|
| 141 | css_class = 's'
|
| 142 | elif str_type == _OTHER_LITERAL:
|
| 143 | css_class = 'o'
|
| 144 | elif str_type == _OTHER_TYPE:
|
| 145 | css_class = 'o'
|
| 146 | else:
|
| 147 | raise AssertionError(str_type)
|
| 148 | self.f.write('<span class="%s">' % css_class)
|
| 149 |
|
| 150 | def PopColor(self):
|
| 151 | self.f.write('</span>')
|
| 152 |
|
| 153 | def write(self, s):
|
| 154 | # PROBLEM: Double escaping!
|
| 155 | self.f.write(cgi.escape(s))
|
| 156 | self.num_chars += len(s) # Only count visible characters!
|
| 157 |
|
| 158 |
|
| 159 | # Color token types
|
| 160 | _NODE_TYPE = 1
|
| 161 | _STRING_LITERAL = 2
|
| 162 | _OTHER_LITERAL = 3 # Int and bool. Green?
|
| 163 | _OTHER_TYPE = 4 # Or
|
| 164 |
|
| 165 |
|
| 166 | # ANSI color constants (also in sh_spec.py)
|
| 167 | _RESET = '\033[0;0m'
|
| 168 | _BOLD = '\033[1m'
|
| 169 |
|
| 170 | _RED = '\033[31m'
|
| 171 | _GREEN = '\033[32m'
|
| 172 | _BLUE = '\033[34m'
|
| 173 |
|
| 174 | _YELLOW = '\033[33m'
|
| 175 | _CYAN = '\033[36m'
|
| 176 |
|
| 177 |
|
| 178 | class AnsiOutput(ColorOutput):
|
| 179 | """For the console."""
|
| 180 |
|
| 181 | def __init__(self, f):
|
| 182 | ColorOutput.__init__(self, f)
|
| 183 |
|
| 184 | def NewTempBuffer(self):
|
| 185 | return AnsiOutput(util.Buffer())
|
| 186 |
|
| 187 | def PushColor(self, str_type):
|
| 188 | if str_type == _NODE_TYPE:
|
| 189 | #self.f.write(_GREEN)
|
| 190 | self.f.write(_YELLOW)
|
| 191 | elif str_type == _STRING_LITERAL:
|
| 192 | self.f.write(_BOLD)
|
| 193 | elif str_type == _OTHER_LITERAL:
|
| 194 | self.f.write(_GREEN)
|
| 195 | elif str_type == _OTHER_TYPE:
|
| 196 | self.f.write(_GREEN) # Same color as other literals for now
|
| 197 | else:
|
| 198 | raise AssertionError(str_type)
|
| 199 |
|
| 200 | def PopColor(self):
|
| 201 | self.f.write(_RESET)
|
| 202 |
|
| 203 |
|
| 204 | #
|
| 205 | # Nodes
|
| 206 | #
|
| 207 |
|
| 208 |
|
| 209 | class _Obj(object):
|
| 210 | """Node for pretty-printing."""
|
| 211 | def __init__(self, node_type):
|
| 212 | self.node_type = node_type
|
| 213 | self.fields = [] # list of 2-tuples of (name, Obj or ColoredString)
|
| 214 |
|
| 215 | # Custom hooks can change these:
|
| 216 | self.abbrev = False
|
| 217 | self.show_node_type = True # only respected when abbrev is false
|
| 218 | self.left = '('
|
| 219 | self.right = ')'
|
| 220 | self.unnamed_fields = [] # if this is set, it's printed instead?
|
| 221 | # problem: CompoundWord just has word_part though
|
| 222 | # List of Obj or ColoredString
|
| 223 |
|
| 224 | def __repr__(self):
|
| 225 | return '<_Obj %s %s>' % (self.node_type, self.fields)
|
| 226 |
|
| 227 |
|
| 228 | class _ColoredString(object):
|
| 229 | """Node for pretty-printing."""
|
| 230 | def __init__(self, s, str_type):
|
| 231 | assert isinstance(s, str), s
|
| 232 | self.s = s
|
| 233 | self.str_type = str_type
|
| 234 |
|
| 235 | def __repr__(self):
|
| 236 | return '<_ColoredString %s %s>' % (self.s, self.str_type)
|
| 237 |
|
| 238 |
|
| 239 | def MakeFieldSubtree(obj, field_name, desc, abbrev_hook, omit_empty=True):
|
| 240 | try:
|
| 241 | field_val = getattr(obj, field_name)
|
| 242 | except AttributeError:
|
| 243 | # This happens when required fields are not initialized, e.g. FuncCall()
|
| 244 | # without setting name.
|
| 245 | raise AssertionError(
|
| 246 | '%s is missing field %r' % (obj.__class__, field_name))
|
| 247 |
|
| 248 | if isinstance(desc, asdl.IntType):
|
| 249 | out_val = _ColoredString(str(field_val), _OTHER_LITERAL)
|
| 250 |
|
| 251 | elif isinstance(desc, asdl.BoolType):
|
| 252 | out_val = _ColoredString('T' if field_val else 'F', _OTHER_LITERAL)
|
| 253 |
|
| 254 | elif isinstance(desc, asdl.Sum) and asdl.is_simple(desc):
|
| 255 | out_val = field_val.name
|
| 256 |
|
| 257 | elif isinstance(desc, asdl.StrType):
|
| 258 | out_val = _ColoredString(field_val, _STRING_LITERAL)
|
| 259 |
|
| 260 | elif isinstance(desc, asdl.ArrayType):
|
| 261 | out_val = []
|
| 262 | obj_list = field_val
|
| 263 | for child_obj in obj_list:
|
| 264 | t = MakeTree(child_obj, abbrev_hook)
|
| 265 | out_val.append(t)
|
| 266 |
|
| 267 | if omit_empty and not obj_list:
|
| 268 | out_val = None
|
| 269 |
|
| 270 | elif isinstance(desc, asdl.MaybeType):
|
| 271 | if field_val is None:
|
| 272 | out_val = None
|
| 273 | else:
|
| 274 | out_val = MakeTree(field_val, abbrev_hook)
|
| 275 |
|
| 276 | else:
|
| 277 | out_val = MakeTree(field_val, abbrev_hook)
|
| 278 |
|
| 279 | return out_val
|
| 280 |
|
| 281 |
|
| 282 | def MakeTree(obj, abbrev_hook=None, omit_empty=True):
|
| 283 | """The first step of printing: create a homogeneous tree.
|
| 284 |
|
| 285 | Args:
|
| 286 | obj: py_meta.Obj
|
| 287 | omit_empty: Whether to omit empty lists
|
| 288 | Returns:
|
| 289 | _Obj node
|
| 290 | """
|
| 291 | from asdl import py_meta
|
| 292 |
|
| 293 | if isinstance(obj, py_meta.SimpleObj): # Primitive
|
| 294 | return obj.name
|
| 295 |
|
| 296 | elif isinstance(obj, py_meta.CompoundObj):
|
| 297 | # These lines can be possibly COMBINED all into one. () can replace
|
| 298 | # indentation?
|
| 299 | out_node = _Obj(obj.__class__.__name__)
|
| 300 |
|
| 301 | for field_name, desc in obj.ASDL_TYPE.GetFields():
|
| 302 | out_val = MakeFieldSubtree(obj, field_name, desc, abbrev_hook,
|
| 303 | omit_empty=omit_empty)
|
| 304 |
|
| 305 | if out_val is not None:
|
| 306 | out_node.fields.append((field_name, out_val))
|
| 307 |
|
| 308 | # Call user-defined hook to abbreviate compound objects.
|
| 309 | if abbrev_hook:
|
| 310 | abbrev_hook(obj, out_node)
|
| 311 |
|
| 312 | elif isinstance(obj, str): # Could be an array of strings
|
| 313 | return _ColoredString(obj, _STRING_LITERAL)
|
| 314 |
|
| 315 | else:
|
| 316 | # Id uses this now. TODO: Should we have plugins? Might need it for
|
| 317 | # color.
|
| 318 | return _ColoredString(repr(obj), _OTHER_TYPE)
|
| 319 |
|
| 320 | return out_node
|
| 321 |
|
| 322 |
|
| 323 | # This is word characters, - and _, as well as path name characters . and /.
|
| 324 | _PLAIN_RE = re.compile(r'^[a-zA-Z0-9\-_./]+$')
|
| 325 |
|
| 326 | # NOTE: Turning JSON back on can be a cheap hack for detecting invalid unicode.
|
| 327 | # But we want to write our own AST walker for that.
|
| 328 |
|
| 329 | def _PrettyString(s):
|
| 330 | if '\n' in s:
|
| 331 | #return json.dumps(s) # account for the fact that $ matches the newline
|
| 332 | return repr(s)
|
| 333 | if _PLAIN_RE.match(s):
|
| 334 | return s
|
| 335 | else:
|
| 336 | #return json.dumps(s)
|
| 337 | return repr(s)
|
| 338 |
|
| 339 |
|
| 340 | INDENT = 2
|
| 341 |
|
| 342 | def _PrintWrappedArray(array, prefix_len, f, indent, max_col):
|
| 343 | """Print an array of objects with line wrapping.
|
| 344 |
|
| 345 | Returns whether they all fit on a single line, so you can print the closing
|
| 346 | brace properly.
|
| 347 | """
|
| 348 | all_fit = True
|
| 349 | chars_so_far = prefix_len
|
| 350 |
|
| 351 | for i, val in enumerate(array):
|
| 352 | if i != 0:
|
| 353 | f.write(' ')
|
| 354 |
|
| 355 | single_f = f.NewTempBuffer()
|
| 356 | if _TrySingleLine(val, single_f, max_col - chars_so_far):
|
| 357 | f.WriteRaw(single_f.GetRaw())
|
| 358 | chars_so_far += single_f.NumChars()
|
| 359 | else: # WRAP THE LINE
|
| 360 | f.write('\n')
|
| 361 | # TODO: Add max_col here, taking into account the field name
|
| 362 | new_indent = indent + INDENT
|
| 363 | PrintTree(val, f, indent=new_indent, max_col=max_col)
|
| 364 |
|
| 365 | chars_so_far = 0 # allow more
|
| 366 | all_fit = False
|
| 367 | return all_fit
|
| 368 |
|
| 369 |
|
| 370 | def _PrintWholeArray(array, prefix_len, f, indent, max_col):
|
| 371 | # This is UNLIKE the abbreviated case above, where we do WRAPPING.
|
| 372 | # Here, ALL children must fit on a single line, or else we separate
|
| 373 | # each one oonto a separate line. This is to avoid the following:
|
| 374 | #
|
| 375 | # children: [(C ...)
|
| 376 | # (C ...)
|
| 377 | # ]
|
| 378 | # The first child is out of line. The abbreviated objects have a
|
| 379 | # small header like C or DQ so it doesn't matter as much.
|
| 380 | all_fit = True
|
| 381 | pieces = []
|
| 382 | chars_so_far = prefix_len
|
| 383 | for item in array:
|
| 384 | single_f = f.NewTempBuffer()
|
| 385 | if _TrySingleLine(item, single_f, max_col - chars_so_far):
|
| 386 | pieces.append(single_f.GetRaw())
|
| 387 | chars_so_far += single_f.NumChars()
|
| 388 | else:
|
| 389 | all_fit = False
|
| 390 | break
|
| 391 |
|
| 392 | if all_fit:
|
| 393 | for i, p in enumerate(pieces):
|
| 394 | if i != 0:
|
| 395 | f.write(' ')
|
| 396 | f.WriteRaw(p)
|
| 397 | f.write(']')
|
| 398 | return all_fit
|
| 399 |
|
| 400 |
|
| 401 | def _PrintTreeObj(node, f, indent, max_col):
|
| 402 | """Print a CompoundObj in abbreviated or normal form."""
|
| 403 | ind = ' ' * indent
|
| 404 |
|
| 405 | if node.abbrev: # abbreviated
|
| 406 | prefix = ind + node.left
|
| 407 | f.write(prefix)
|
| 408 | if node.show_node_type:
|
| 409 | f.PushColor(_NODE_TYPE)
|
| 410 | f.write(node.node_type)
|
| 411 | f.PopColor()
|
| 412 | f.write(' ')
|
| 413 |
|
| 414 | prefix_len = len(prefix) + len(node.node_type) + 1
|
| 415 | all_fit = _PrintWrappedArray(
|
| 416 | node.unnamed_fields, prefix_len, f, indent, max_col)
|
| 417 |
|
| 418 | if not all_fit:
|
| 419 | f.write('\n')
|
| 420 | f.write(ind)
|
| 421 | f.write(node.right)
|
| 422 |
|
| 423 | else: # full form like (SimpleCommand ...)
|
| 424 | f.write(ind + node.left)
|
| 425 |
|
| 426 | f.PushColor(_NODE_TYPE)
|
| 427 | f.write(node.node_type)
|
| 428 | f.PopColor()
|
| 429 |
|
| 430 | f.write('\n')
|
| 431 | i = 0
|
| 432 | for name, val in node.fields:
|
| 433 | ind1 = ' ' * (indent+INDENT)
|
| 434 | if isinstance(val, list): # list field
|
| 435 | name_str = '%s%s: [' % (ind1, name)
|
| 436 | f.write(name_str)
|
| 437 | prefix_len = len(name_str)
|
| 438 |
|
| 439 | if not _PrintWholeArray(val, prefix_len, f, indent, max_col):
|
| 440 | f.write('\n')
|
| 441 | for child in val:
|
| 442 | # TODO: Add max_col here
|
| 443 | PrintTree(child, f, indent=indent+INDENT+INDENT)
|
| 444 | f.write('\n')
|
| 445 | f.write('%s]' % ind1)
|
| 446 |
|
| 447 | else: # primitive field
|
| 448 | name_str = '%s%s: ' % (ind1, name)
|
| 449 | f.write(name_str)
|
| 450 | prefix_len = len(name_str)
|
| 451 |
|
| 452 | # Try to print it on the same line as the field name; otherwise print
|
| 453 | # it on a separate line.
|
| 454 | single_f = f.NewTempBuffer()
|
| 455 | if _TrySingleLine(val, single_f, max_col - prefix_len):
|
| 456 | f.WriteRaw(single_f.GetRaw())
|
| 457 | else:
|
| 458 | f.write('\n')
|
| 459 | # TODO: Add max_col here, taking into account the field name
|
| 460 | PrintTree(val, f, indent=indent+INDENT+INDENT)
|
| 461 | i += 1
|
| 462 |
|
| 463 | f.write('\n') # separate fields
|
| 464 |
|
| 465 | f.write(ind + node.right)
|
| 466 |
|
| 467 |
|
| 468 | def PrintTree(node, f, indent=0, max_col=100):
|
| 469 | """Second step of printing: turn homogeneous tree into a colored string.
|
| 470 |
|
| 471 | Args:
|
| 472 | node: homogeneous tree node
|
| 473 | f: ColorOutput instance.
|
| 474 | max_col: don't print past this column number on ANY line
|
| 475 | """
|
| 476 | ind = ' ' * indent
|
| 477 |
|
| 478 | # Try printing on a single line
|
| 479 | single_f = f.NewTempBuffer()
|
| 480 | single_f.write(ind)
|
| 481 | if _TrySingleLine(node, single_f, max_col - indent):
|
| 482 | f.WriteRaw(single_f.GetRaw())
|
| 483 | return
|
| 484 |
|
| 485 | if isinstance(node, str):
|
| 486 | f.write(ind + _PrettyString(node))
|
| 487 |
|
| 488 | elif isinstance(node, _ColoredString):
|
| 489 | f.PushColor(node.str_type)
|
| 490 | f.write(_PrettyString(node.s))
|
| 491 | f.PopColor()
|
| 492 |
|
| 493 | elif isinstance(node, _Obj):
|
| 494 | _PrintTreeObj(node, f, indent, max_col)
|
| 495 |
|
| 496 | else:
|
| 497 | raise AssertionError(node)
|
| 498 |
|
| 499 |
|
| 500 | def _TrySingleLineObj(node, f, max_chars):
|
| 501 | """Print an object on a single line."""
|
| 502 | f.write(node.left)
|
| 503 | if node.abbrev:
|
| 504 | if node.show_node_type:
|
| 505 | f.PushColor(_NODE_TYPE)
|
| 506 | f.write(node.node_type)
|
| 507 | f.PopColor()
|
| 508 | f.write(' ')
|
| 509 |
|
| 510 | for i, val in enumerate(node.unnamed_fields):
|
| 511 | if i != 0:
|
| 512 | f.write(' ')
|
| 513 | if not _TrySingleLine(val, f, max_chars):
|
| 514 | return False
|
| 515 | else:
|
| 516 | f.PushColor(_NODE_TYPE)
|
| 517 | f.write(node.node_type)
|
| 518 | f.PopColor()
|
| 519 |
|
| 520 | for name, val in node.fields:
|
| 521 | f.write(' %s:' % name)
|
| 522 | if not _TrySingleLine(val, f, max_chars):
|
| 523 | return False
|
| 524 |
|
| 525 | f.write(node.right)
|
| 526 | return True
|
| 527 |
|
| 528 |
|
| 529 | def _TrySingleLine(node, f, max_chars):
|
| 530 | """Try printing on a single line.
|
| 531 |
|
| 532 | Args:
|
| 533 | node: homogeneous tree node
|
| 534 | f: ColorOutput instance
|
| 535 | max_chars: maximum number of characters to print on THIS line
|
| 536 | indent: current indent level
|
| 537 |
|
| 538 | Returns:
|
| 539 | ok: whether it fit on the line of the given size.
|
| 540 | If False, you can't use the value of f.
|
| 541 | """
|
| 542 | if isinstance(node, str):
|
| 543 | f.write(_PrettyString(node))
|
| 544 |
|
| 545 | elif isinstance(node, _ColoredString):
|
| 546 | f.PushColor(node.str_type)
|
| 547 | f.write(_PrettyString(node.s))
|
| 548 | f.PopColor()
|
| 549 |
|
| 550 | elif isinstance(node, list): # Can we fit the WHOLE list on the line?
|
| 551 | f.write('[')
|
| 552 | for i, item in enumerate(node):
|
| 553 | if i != 0:
|
| 554 | f.write(' ')
|
| 555 | if not _TrySingleLine(item, f, max_chars):
|
| 556 | return False
|
| 557 | f.write(']')
|
| 558 |
|
| 559 | elif isinstance(node, _Obj):
|
| 560 | return _TrySingleLineObj(node, f, max_chars)
|
| 561 |
|
| 562 | else:
|
| 563 | raise AssertionError("Unexpected node: %r (%r)" % (node, node.__class__))
|
| 564 |
|
| 565 | # Take into account the last char.
|
| 566 | num_chars_so_far = f.NumChars()
|
| 567 | if num_chars_so_far > max_chars:
|
| 568 | return False
|
| 569 |
|
| 570 | return True
|