pyparse.py 19.6 KB
Newer Older
1 2
"""Define partial Python code Parser used by editor and hyperparser.

3
Instances of ParseMap are used with str.translate.
4 5 6 7 8 9 10 11 12

The following bound search and match functions are defined:
_synchre - start of popular statement;
_junkre - whitespace or comment line;
_match_stringre: string, possibly without closer;
_itemre - line that may have bracket structure start;
_closere - line that must be followed by dedent.
_chew_ordinaryre - non-special characters.
"""
David Scherer's avatar
David Scherer committed
13 14
import re

15
# Reason last statement is continued (or C_NONE if it's not).
16 17
(C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE,
 C_STRING_NEXT_LINES, C_BRACKET) = range(5)
David Scherer's avatar
David Scherer committed
18

19
# Find what looks like the start of a popular statement.
David Scherer's avatar
David Scherer committed
20 21 22 23

_synchre = re.compile(r"""
    ^
    [ \t]*
24
    (?: while
David Scherer's avatar
David Scherer committed
25 26 27 28 29 30 31 32 33 34 35 36
    |   else
    |   def
    |   return
    |   assert
    |   break
    |   class
    |   continue
    |   elif
    |   try
    |   except
    |   raise
    |   import
37
    |   yield
David Scherer's avatar
David Scherer committed
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
    )
    \b
""", re.VERBOSE | re.MULTILINE).search

# Match blank line or non-indenting comment line.

_junkre = re.compile(r"""
    [ \t]*
    (?: \# \S .* )?
    \n
""", re.VERBOSE).match

# Match any flavor of string; the terminating quote is optional
# so that we're robust in the face of incomplete program text.

_match_stringre = re.compile(r"""
    \""" [^"\\]* (?:
                     (?: \\. | "(?!"") )
                     [^"\\]*
                 )*
    (?: \""" )?

|   " [^"\\\n]* (?: \\. [^"\\\n]* )* "?

|   ''' [^'\\]* (?:
                   (?: \\. | '(?!'') )
                   [^'\\]*
                )*
    (?: ''' )?

|   ' [^'\\\n]* (?: \\. [^'\\\n]* )* '?
""", re.VERBOSE | re.DOTALL).match

# Match a line that starts with something interesting;
# used to find the first item of a bracket structure.

_itemre = re.compile(r"""
    [ \t]*
    [^\s#\\]    # if we match, m.end()-1 is the interesting char
""", re.VERBOSE).match

79
# Match start of statements that should be followed by a dedent.
David Scherer's avatar
David Scherer committed
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

_closere = re.compile(r"""
    \s*
    (?: return
    |   break
    |   continue
    |   raise
    |   pass
    )
    \b
""", re.VERBOSE).match

# Chew up non-special chars as quickly as possible.  If match is
# successful, m.end() less 1 is the index of the last boring char
# matched.  If match is unsuccessful, the string starts with an
# interesting char.

_chew_ordinaryre = re.compile(r"""
    [^[\](){}#'"\\]+
""", re.VERBOSE).match


102 103
class ParseMap(dict):
    r"""Dict subclass that maps anything not in dict to 'x'.
104

105 106 107
    This is designed to be used with str.translate in study1.
    Anything not specifically mapped otherwise becomes 'x'.
    Example: replace everything except whitespace with 'x'.
108

109 110
    >>> keepwhite = ParseMap((ord(c), ord(c)) for c in ' \t\n\r')
    >>> "a + b\tc\nd".translate(keepwhite)
111 112
    'x x x\tx\nx'
    """
113 114 115
    # Calling this triples access time; see bpo-32940
    def __missing__(self, key):
        return 120  # ord('x')
116 117


118 119 120 121 122
# Map all ascii to 120 to avoid __missing__ call, then replace some.
trans = ParseMap.fromkeys(range(128), 120)
trans.update((ord(c), ord('(')) for c in "({[")  # open brackets => '(';
trans.update((ord(c), ord(')')) for c in ")}]")  # close brackets => ')'.
trans.update((ord(c), ord(c)) for c in "\"'\\\n#")  # Keep these.
123

David Scherer's avatar
David Scherer committed
124 125 126 127 128 129 130

class Parser:

    def __init__(self, indentwidth, tabwidth):
        self.indentwidth = indentwidth
        self.tabwidth = tabwidth

131
    def set_code(self, s):
132
        assert len(s) == 0 or s[-1] == '\n'
133
        self.code = s
David Scherer's avatar
David Scherer committed
134 135
        self.study_level = 0

136
    def find_good_parse_start(self, is_char_in_string=None,
David Scherer's avatar
David Scherer committed
137
                              _synchre=_synchre):
138 139 140 141 142 143 144 145 146 147 148 149
        """
        Return index of a good place to begin parsing, as close to the
        end of the string as possible.  This will be the start of some
        popular stmt like "if" or "def".  Return None if none found:
        the caller should pass more prior context then, if possible, or
        if not (the entire program text up until the point of interest
        has already been tried) pass 0 to set_lo().

        This will be reliable iff given a reliable is_char_in_string()
        function, meaning that when it says "no", it's absolutely
        guaranteed that the char is not in a string.
        """
150
        code, pos = self.code, None
David Scherer's avatar
David Scherer committed
151 152 153 154 155 156 157 158

        if not is_char_in_string:
            # no clue -- make the caller pass everything
            return None

        # Peek back from the end for a good place to start,
        # but don't try too often; pos will be left None, or
        # bumped to a legitimate synch point.
159
        limit = len(code)
David Scherer's avatar
David Scherer committed
160
        for tries in range(5):
161
            i = code.rfind(":\n", 0, limit)
David Scherer's avatar
David Scherer committed
162 163
            if i < 0:
                break
164 165
            i = code.rfind('\n', 0, i) + 1  # start of colon line (-1+1=0)
            m = _synchre(code, i, limit)
David Scherer's avatar
David Scherer committed
166 167 168 169 170 171 172 173 174 175 176 177 178
            if m and not is_char_in_string(m.start()):
                pos = m.start()
                break
            limit = i
        if pos is None:
            # Nothing looks like a block-opener, or stuff does
            # but is_char_in_string keeps returning true; most likely
            # we're in or near a giant string, the colorizer hasn't
            # caught up enough to be helpful, or there simply *aren't*
            # any interesting stmts.  In any of these cases we're
            # going to have to parse the whole thing to be sure, so
            # give it one last try from the start, but stop wasting
            # time here regardless of the outcome.
179
            m = _synchre(code)
David Scherer's avatar
David Scherer committed
180 181 182 183 184 185 186 187
            if m and not is_char_in_string(m.start()):
                pos = m.start()
            return pos

        # Peeking back worked; look forward until _synchre no longer
        # matches.
        i = pos + 1
        while 1:
188
            m = _synchre(code, i)
David Scherer's avatar
David Scherer committed
189 190 191 192 193 194 195 196 197
            if m:
                s, i = m.span()
                if not is_char_in_string(s):
                    pos = s
            else:
                break
        return pos

    def set_lo(self, lo):
198 199 200 201
        """ Throw away the start of the string.

        Intended to be called with the result of find_good_parse_start().
        """
202
        assert lo == 0 or self.code[lo-1] == '\n'
David Scherer's avatar
David Scherer committed
203
        if lo > 0:
204
            self.code = self.code[lo:]
David Scherer's avatar
David Scherer committed
205

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
206
    def _study1(self):
207 208 209 210 211 212
        """Find the line numbers of non-continuation lines.

        As quickly as humanly possible <wink>, find the line numbers (0-
        based) of the non-continuation lines.
        Creates self.{goodlines, continuation}.
        """
David Scherer's avatar
David Scherer committed
213 214 215 216 217 218 219 220
        if self.study_level >= 1:
            return
        self.study_level = 1

        # Map all uninteresting characters to "x", all open brackets
        # to "(", all close brackets to ")", then collapse runs of
        # uninteresting characters.  This can cut the number of chars
        # by a factor of 10-40, and so greatly speed the following loop.
221
        code = self.code
222
        code = code.translate(trans)
223 224 225 226 227
        code = code.replace('xxxxxxxx', 'x')
        code = code.replace('xxxx', 'x')
        code = code.replace('xx', 'x')
        code = code.replace('xx', 'x')
        code = code.replace('\nx', '\n')
228 229
        # Replacing x\n with \n would be incorrect because
        # x may be preceded by a backslash.
David Scherer's avatar
David Scherer committed
230 231 232 233 234 235 236 237

        # March over the squashed version of the program, accumulating
        # the line numbers of non-continued stmts, and determining
        # whether & why the last stmt is a continuation.
        continuation = C_NONE
        level = lno = 0     # level is nesting level; lno is line number
        self.goodlines = goodlines = [0]
        push_good = goodlines.append
238
        i, n = 0, len(code)
David Scherer's avatar
David Scherer committed
239
        while i < n:
240
            ch = code[i]
David Scherer's avatar
David Scherer committed
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
            i = i+1

            # cases are checked in decreasing order of frequency
            if ch == 'x':
                continue

            if ch == '\n':
                lno = lno + 1
                if level == 0:
                    push_good(lno)
                    # else we're in an unclosed bracket structure
                continue

            if ch == '(':
                level = level + 1
                continue

            if ch == ')':
                if level:
                    level = level - 1
                    # else the program is invalid, but we can't complain
                continue

            if ch == '"' or ch == "'":
                # consume the string
                quote = ch
267
                if code[i-1:i+2] == quote * 3:
David Scherer's avatar
David Scherer committed
268
                    quote = quote * 3
269
                firstlno = lno
David Scherer's avatar
David Scherer committed
270 271 272
                w = len(quote) - 1
                i = i+w
                while i < n:
273
                    ch = code[i]
David Scherer's avatar
David Scherer committed
274 275 276 277 278
                    i = i+1

                    if ch == 'x':
                        continue

279
                    if code[i-1:i+w] == quote:
David Scherer's avatar
David Scherer committed
280 281 282 283 284 285 286 287 288 289 290 291 292 293
                        i = i+w
                        break

                    if ch == '\n':
                        lno = lno + 1
                        if w == 0:
                            # unterminated single-quoted string
                            if level == 0:
                                push_good(lno)
                            break
                        continue

                    if ch == '\\':
                        assert i < n
294
                        if code[i] == '\n':
David Scherer's avatar
David Scherer committed
295 296 297 298 299 300 301 302 303
                            lno = lno + 1
                        i = i+1
                        continue

                    # else comment char or paren inside string

                else:
                    # didn't break out of the loop, so we're still
                    # inside a string
304
                    if (lno - 1) == firstlno:
305
                        # before the previous \n in code, we were in the first
306 307 308 309
                        # line of the string
                        continuation = C_STRING_FIRST_LINE
                    else:
                        continuation = C_STRING_NEXT_LINES
David Scherer's avatar
David Scherer committed
310 311 312 313
                continue    # with outer loop

            if ch == '#':
                # consume the comment
314
                i = code.find('\n', i)
David Scherer's avatar
David Scherer committed
315 316 317 318 319
                assert i >= 0
                continue

            assert ch == '\\'
            assert i < n
320
            if code[i] == '\n':
David Scherer's avatar
David Scherer committed
321 322 323 324 325 326 327 328
                lno = lno + 1
                if i+1 == n:
                    continuation = C_BACKSLASH
            i = i+1

        # The last stmt may be continued for all 3 reasons.
        # String continuation takes precedence over bracket
        # continuation, which beats backslash continuation.
329 330
        if (continuation != C_STRING_FIRST_LINE
            and continuation != C_STRING_NEXT_LINES and level > 0):
David Scherer's avatar
David Scherer committed
331 332 333 334 335 336 337 338 339 340 341 342 343
            continuation = C_BRACKET
        self.continuation = continuation

        # Push the final line number as a sentinel value, regardless of
        # whether it's continued.
        assert (continuation == C_NONE) == (goodlines[-1] == lno)
        if goodlines[-1] != lno:
            push_good(lno)

    def get_continuation_type(self):
        self._study1()
        return self.continuation

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
344
    def _study2(self):
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        """
        study1 was sufficient to determine the continuation status,
        but doing more requires looking at every character.  study2
        does this for the last interesting statement in the block.
        Creates:
            self.stmt_start, stmt_end
                slice indices of last interesting stmt
            self.stmt_bracketing
                the bracketing structure of the last interesting stmt; for
                example, for the statement "say(boo) or die",
                stmt_bracketing will be ((0, 0), (0, 1), (2, 0), (2, 1),
                (4, 0)). Strings and comments are treated as brackets, for
                the matter.
            self.lastch
                last interesting character before optional trailing comment
            self.lastopenbracketpos
                if continuation is C_BRACKET, index of last open bracket
        """
David Scherer's avatar
David Scherer committed
363 364 365 366 367 368
        if self.study_level >= 2:
            return
        self._study1()
        self.study_level = 2

        # Set p and q to slice indices of last interesting stmt.
369
        code, goodlines = self.code, self.goodlines
370
        i = len(goodlines) - 1  # Index of newest line.
371
        p = len(code)  # End of goodlines[i]
David Scherer's avatar
David Scherer committed
372 373
        while i:
            assert p
374
            # Make p be the index of the stmt at line number goodlines[i].
David Scherer's avatar
David Scherer committed
375 376 377 378
            # Move p back to the stmt at line number goodlines[i-1].
            q = p
            for nothing in range(goodlines[i-1], goodlines[i]):
                # tricky: sets p to 0 if no preceding newline
379 380
                p = code.rfind('\n', 0, p-1) + 1
            # The stmt code[p:q] isn't a continuation, but may be blank
David Scherer's avatar
David Scherer committed
381
            # or a non-indenting comment line.
382
            if  _junkre(code, p):
David Scherer's avatar
David Scherer committed
383 384 385 386 387 388 389 390 391 392 393 394 395 396
                i = i-1
            else:
                break
        if i == 0:
            # nothing but junk!
            assert p == 0
            q = p
        self.stmt_start, self.stmt_end = p, q

        # Analyze this stmt, to find the last open bracket (if any)
        # and last interesting character (if any).
        lastch = ""
        stack = []  # stack of open bracket indices
        push_stack = stack.append
397
        bracketing = [(p, 0)]
David Scherer's avatar
David Scherer committed
398 399
        while p < q:
            # suck up all except ()[]{}'"#\\
400
            m = _chew_ordinaryre(code, p, q)
David Scherer's avatar
David Scherer committed
401 402
            if m:
                # we skipped at least one boring char
403
                newp = m.end()
David Scherer's avatar
David Scherer committed
404
                # back up over totally boring whitespace
405
                i = newp - 1    # index of last boring char
406
                while i >= p and code[i] in " \t\n":
David Scherer's avatar
David Scherer committed
407
                    i = i-1
408
                if i >= p:
409
                    lastch = code[i]
410
                p = newp
David Scherer's avatar
David Scherer committed
411 412 413
                if p >= q:
                    break

414
            ch = code[p]
David Scherer's avatar
David Scherer committed
415 416 417

            if ch in "([{":
                push_stack(p)
418
                bracketing.append((p, len(stack)))
David Scherer's avatar
David Scherer committed
419 420 421 422 423 424 425 426 427
                lastch = ch
                p = p+1
                continue

            if ch in ")]}":
                if stack:
                    del stack[-1]
                lastch = ch
                p = p+1
428
                bracketing.append((p, len(stack)))
David Scherer's avatar
David Scherer committed
429 430 431 432 433 434 435 436 437 438
                continue

            if ch == '"' or ch == "'":
                # consume string
                # Note that study1 did this with a Python loop, but
                # we use a regexp here; the reason is speed in both
                # cases; the string may be huge, but study1 pre-squashed
                # strings to a couple of characters per line.  study1
                # also needed to keep track of newlines, and we don't
                # have to.
439
                bracketing.append((p, len(stack)+1))
David Scherer's avatar
David Scherer committed
440
                lastch = ch
441
                p = _match_stringre(code, p, q).end()
442
                bracketing.append((p, len(stack)))
David Scherer's avatar
David Scherer committed
443 444 445 446
                continue

            if ch == '#':
                # consume comment and trailing newline
447
                bracketing.append((p, len(stack)+1))
448
                p = code.find('\n', p, q) + 1
David Scherer's avatar
David Scherer committed
449
                assert p > 0
450
                bracketing.append((p, len(stack)))
David Scherer's avatar
David Scherer committed
451 452 453 454 455
                continue

            assert ch == '\\'
            p = p+1     # beyond backslash
            assert p < q
456
            if code[p] != '\n':
David Scherer's avatar
David Scherer committed
457
                # the program is invalid, but can't complain
458
                lastch = ch + code[p]
David Scherer's avatar
David Scherer committed
459 460 461 462 463
            p = p+1     # beyond escaped char

        # end while p < q:

        self.lastch = lastch
464
        self.lastopenbracketpos = stack[-1] if stack else None
465
        self.stmt_bracketing = tuple(bracketing)
David Scherer's avatar
David Scherer committed
466

Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
467
    def compute_bracket_indent(self):
468 469 470 471
        """Return number of spaces the next line should be indented.

        Line continuation must be C_BRACKET.
        """
David Scherer's avatar
David Scherer committed
472 473 474
        self._study2()
        assert self.continuation == C_BRACKET
        j = self.lastopenbracketpos
475 476 477
        code = self.code
        n = len(code)
        origi = i = code.rfind('\n', 0, j) + 1
David Scherer's avatar
David Scherer committed
478 479 480
        j = j+1     # one beyond open bracket
        # find first list item; set i to start of its line
        while j < n:
481
            m = _itemre(code, j)
David Scherer's avatar
David Scherer committed
482 483 484 485 486 487
            if m:
                j = m.end() - 1     # index of first interesting char
                extra = 0
                break
            else:
                # this line is junk; advance to next line
488
                i = j = code.find('\n', j) + 1
David Scherer's avatar
David Scherer committed
489 490 491 492
        else:
            # nothing interesting follows the bracket;
            # reproduce the bracket line's indentation + a level
            j = i = origi
493
            while code[j] in " \t":
David Scherer's avatar
David Scherer committed
494 495
                j = j+1
            extra = self.indentwidth
496
        return len(code[i:j].expandtabs(self.tabwidth)) + extra
David Scherer's avatar
David Scherer committed
497 498

    def get_num_lines_in_stmt(self):
499 500 501 502 503
        """Return number of physical lines in last stmt.

        The statement doesn't have to be an interesting statement.  This is
        intended to be called when continuation is C_BACKSLASH.
        """
David Scherer's avatar
David Scherer committed
504 505 506 507 508
        self._study1()
        goodlines = self.goodlines
        return goodlines[-1] - goodlines[-2]

    def compute_backslash_indent(self):
509 510 511 512 513
        """Return number of spaces the next line should be indented.

        Line continuation must be C_BACKSLASH.  Also assume that the new
        line is the first one following the initial line of the stmt.
        """
David Scherer's avatar
David Scherer committed
514 515
        self._study2()
        assert self.continuation == C_BACKSLASH
516
        code = self.code
David Scherer's avatar
David Scherer committed
517
        i = self.stmt_start
518
        while code[i] in " \t":
David Scherer's avatar
David Scherer committed
519 520 521 522 523
            i = i+1
        startpos = i

        # See whether the initial line starts an assignment stmt; i.e.,
        # look for an = operator
524
        endpos = code.find('\n', startpos) + 1
David Scherer's avatar
David Scherer committed
525 526
        found = level = 0
        while i < endpos:
527
            ch = code[i]
David Scherer's avatar
David Scherer committed
528 529 530 531 532 533 534 535
            if ch in "([{":
                level = level + 1
                i = i+1
            elif ch in ")]}":
                if level:
                    level = level - 1
                i = i+1
            elif ch == '"' or ch == "'":
536
                i = _match_stringre(code, i, endpos).end()
David Scherer's avatar
David Scherer committed
537
            elif ch == '#':
538 539
                # This line is unreachable because the # makes a comment of
                # everything after it.
David Scherer's avatar
David Scherer committed
540 541
                break
            elif level == 0 and ch == '=' and \
542 543
                   (i == 0 or code[i-1] not in "=<>!") and \
                   code[i+1] != '=':
David Scherer's avatar
David Scherer committed
544 545 546 547 548 549 550 551 552
                found = 1
                break
            else:
                i = i+1

        if found:
            # found a legit =, but it may be the last interesting
            # thing on the line
            i = i+1     # move beyond the =
553
            found = re.match(r"\s*\\", code[i:endpos]) is None
David Scherer's avatar
David Scherer committed
554 555 556 557 558

        if not found:
            # oh well ... settle for moving beyond the first chunk
            # of non-whitespace chars
            i = startpos
559
            while code[i] not in " \t\n":
David Scherer's avatar
David Scherer committed
560 561
                i = i+1

562
        return len(code[self.stmt_start:i].expandtabs(\
David Scherer's avatar
David Scherer committed
563 564 565
                                     self.tabwidth)) + 1

    def get_base_indent_string(self):
566 567 568
        """Return the leading whitespace on the initial line of the last
        interesting stmt.
        """
David Scherer's avatar
David Scherer committed
569 570 571
        self._study2()
        i, n = self.stmt_start, self.stmt_end
        j = i
572 573
        code = self.code
        while j < n and code[j] in " \t":
David Scherer's avatar
David Scherer committed
574
            j = j + 1
575
        return code[i:j]
David Scherer's avatar
David Scherer committed
576 577

    def is_block_opener(self):
578
        "Return True if the last interesting statemtent opens a block."
David Scherer's avatar
David Scherer committed
579 580 581 582
        self._study2()
        return self.lastch == ':'

    def is_block_closer(self):
583
        "Return True if the last interesting statement closes a block."
David Scherer's avatar
David Scherer committed
584
        self._study2()
585
        return _closere(self.code, self.stmt_start) is not None
David Scherer's avatar
David Scherer committed
586

587
    def get_last_stmt_bracketing(self):
588
        """Return bracketing structure of the last interesting statement.
589

590
        The returned tuple is in the format defined in _study2().
591
        """
592 593
        self._study2()
        return self.stmt_bracketing
594 595


596 597 598
if __name__ == '__main__':
    from unittest import main
    main('idlelib.idle_test.test_pyparse', verbosity=2)