1import rope.base.codeanalyze
2import rope.base.evaluate
3from rope.base import exceptions
4from rope.base import libutils
5from rope.base import utils
6from rope.base import worder
7from rope.base.codeanalyze import ArrayLinesAdapter, LogicalLineFinder
8
9
10class FixSyntax(object):
11
12    def __init__(self, project, code, resource, maxfixes=1):
13        self.project = project
14        self.code = code
15        self.resource = resource
16        self.maxfixes = maxfixes
17
18    @utils.saveit
19    def get_pymodule(self):
20        """Get a `PyModule`"""
21        msg = None
22        code = self.code
23        tries = 0
24        while True:
25            try:
26                if tries == 0 and self.resource is not None and \
27                   self.resource.read() == code:
28                    return self.project.get_pymodule(self.resource,
29                                                     force_errors=True)
30                return libutils.get_string_module(
31                    self.project, code, resource=self.resource,
32                    force_errors=True)
33            except exceptions.ModuleSyntaxError as e:
34                if msg is None:
35                    msg = '%s:%s %s' % (e.filename, e.lineno, e.message_)
36                if tries < self.maxfixes:
37                    tries += 1
38                    self.commenter.comment(e.lineno)
39                    code = '\n'.join(self.commenter.lines)
40                else:
41                    raise exceptions.ModuleSyntaxError(
42                        e.filename, e.lineno,
43                        'Failed to fix error: {0}'.format(msg))
44
45    @property
46    @utils.saveit
47    def commenter(self):
48        return _Commenter(self.code)
49
50    def pyname_at(self, offset):
51        pymodule = self.get_pymodule()
52
53        def old_pyname():
54            word_finder = worder.Worder(self.code, True)
55            expression = word_finder.get_primary_at(offset)
56            expression = expression.replace('\\\n', ' ').replace('\n', ' ')
57            lineno = self.code.count('\n', 0, offset)
58            scope = pymodule.get_scope().get_inner_scope_for_line(lineno)
59            return rope.base.evaluate.eval_str(scope, expression)
60        new_code = pymodule.source_code
61
62        def new_pyname():
63            newoffset = self.commenter.transfered_offset(offset)
64            return rope.base.evaluate.eval_location(pymodule, newoffset)
65        if new_code.startswith(self.code[:offset + 1]):
66            return new_pyname()
67        result = old_pyname()
68        if result is None:
69            return new_pyname()
70        return result
71
72
73class _Commenter(object):
74
75    def __init__(self, code):
76        self.code = code
77        self.lines = self.code.split('\n')
78        self.lines.append('\n')
79        self.origs = list(range(len(self.lines) + 1))
80        self.diffs = [0] * (len(self.lines) + 1)
81
82    def comment(self, lineno):
83        start = _logical_start(self.lines, lineno, check_prev=True) - 1
84        # using self._get_stmt_end() instead of self._get_block_end()
85        # to lower commented lines
86        end = self._get_stmt_end(start)
87        indents = _get_line_indents(self.lines[start])
88        if 0 < start:
89            last_lineno = self._last_non_blank(start - 1)
90            last_line = self.lines[last_lineno]
91            if last_line.rstrip().endswith(':'):
92                indents = _get_line_indents(last_line) + 4
93        self._set(start, ' ' * indents + 'pass')
94        for line in range(start + 1, end + 1):
95            self._set(line, self.lines[start])
96        self._fix_incomplete_try_blocks(lineno, indents)
97
98    def transfered_offset(self, offset):
99        lineno = self.code.count('\n', 0, offset)
100        diff = sum(self.diffs[:lineno])
101        return offset + diff
102
103    def _last_non_blank(self, start):
104        while start > 0 and self.lines[start].strip() == '':
105            start -= 1
106        return start
107
108    def _get_block_end(self, lineno):
109        end_line = lineno
110        base_indents = _get_line_indents(self.lines[lineno])
111        for i in range(lineno + 1, len(self.lines)):
112            if _get_line_indents(self.lines[i]) >= base_indents:
113                end_line = i
114            else:
115                break
116        return end_line
117
118    def _get_stmt_end(self, lineno):
119        base_indents = _get_line_indents(self.lines[lineno])
120        for i in range(lineno + 1, len(self.lines)):
121            if _get_line_indents(self.lines[i]) <= base_indents:
122                return i - 1
123        return lineno
124
125    def _fix_incomplete_try_blocks(self, lineno, indents):
126        block_start = lineno
127        last_indents = indents
128        while block_start > 0:
129            block_start = rope.base.codeanalyze.get_block_start(
130                ArrayLinesAdapter(self.lines), block_start) - 1
131            if self.lines[block_start].strip().startswith('try:'):
132                indents = _get_line_indents(self.lines[block_start])
133                if indents > last_indents:
134                    continue
135                last_indents = indents
136                block_end = self._find_matching_deindent(block_start)
137                line = self.lines[block_end].strip()
138                if not (line.startswith('finally:') or
139                        line.startswith('except ') or
140                        line.startswith('except:')):
141                    self._insert(block_end, ' ' * indents + 'finally:')
142                    self._insert(block_end + 1, ' ' * indents + '    pass')
143
144    def _find_matching_deindent(self, line_number):
145        indents = _get_line_indents(self.lines[line_number])
146        current_line = line_number + 1
147        while current_line < len(self.lines):
148            line = self.lines[current_line]
149            if not line.strip().startswith('#') and not line.strip() == '':
150                # HACK: We should have used logical lines here
151                if _get_line_indents(self.lines[current_line]) <= indents:
152                    return current_line
153            current_line += 1
154        return len(self.lines) - 1
155
156    def _set(self, lineno, line):
157        self.diffs[self.origs[lineno]] += len(line) - len(self.lines[lineno])
158        self.lines[lineno] = line
159
160    def _insert(self, lineno, line):
161        self.diffs[self.origs[lineno]] += len(line) + 1
162        self.origs.insert(lineno, self.origs[lineno])
163        self.lines.insert(lineno, line)
164
165
166def _logical_start(lines, lineno, check_prev=False):
167    logical_finder = LogicalLineFinder(ArrayLinesAdapter(lines))
168    if check_prev:
169        prev = lineno - 1
170        while prev > 0:
171            start, end = logical_finder.logical_line_in(prev)
172            if end is None or start <= lineno < end:
173                return start
174            if start <= prev:
175                break
176            prev -= 1
177    return logical_finder.logical_line_in(lineno)[0]
178
179
180def _get_line_indents(line):
181    return rope.base.codeanalyze.count_line_indents(line)
182