Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
marvel
GitHub Repository: marvel/qnf
Path: blob/master/elisp/emacs-for-python/rope-dist/rope/base/codeanalyze.py
1415 views
1
import bisect
2
import re
3
import token
4
import tokenize
5
6
7
class ChangeCollector(object):
8
9
def __init__(self, text):
10
self.text = text
11
self.changes = []
12
13
def add_change(self, start, end, new_text=None):
14
if new_text is None:
15
new_text = self.text[start:end]
16
self.changes.append((start, end, new_text))
17
18
def get_changed(self):
19
if not self.changes:
20
return None
21
def compare_changes(change1, change2):
22
return cmp(change1[:2], change2[:2])
23
self.changes.sort(compare_changes)
24
pieces = []
25
last_changed = 0
26
for change in self.changes:
27
start, end, text = change
28
pieces.append(self.text[last_changed:start] + text)
29
last_changed = end
30
if last_changed < len(self.text):
31
pieces.append(self.text[last_changed:])
32
result = ''.join(pieces)
33
if result != self.text:
34
return result
35
36
37
class SourceLinesAdapter(object):
38
"""Adapts source to Lines interface
39
40
Note: The creation of this class is expensive.
41
"""
42
43
def __init__(self, source_code):
44
self.code = source_code
45
self.starts = None
46
self._initialize_line_starts()
47
48
def _initialize_line_starts(self):
49
self.starts = []
50
self.starts.append(0)
51
try:
52
i = 0
53
while True:
54
i = self.code.index('\n', i) + 1
55
self.starts.append(i)
56
except ValueError:
57
pass
58
self.starts.append(len(self.code) + 1)
59
60
def get_line(self, lineno):
61
return self.code[self.starts[lineno - 1]:
62
self.starts[lineno] - 1]
63
64
def length(self):
65
return len(self.starts) - 1
66
67
def get_line_number(self, offset):
68
return bisect.bisect(self.starts, offset)
69
70
def get_line_start(self, lineno):
71
return self.starts[lineno - 1]
72
73
def get_line_end(self, lineno):
74
return self.starts[lineno] - 1
75
76
77
class ArrayLinesAdapter(object):
78
79
def __init__(self, lines):
80
self.lines = lines
81
82
def get_line(self, line_number):
83
return self.lines[line_number - 1]
84
85
def length(self):
86
return len(self.lines)
87
88
89
class LinesToReadline(object):
90
91
def __init__(self, lines, start):
92
self.lines = lines
93
self.current = start
94
95
def readline(self):
96
if self.current <= self.lines.length():
97
self.current += 1
98
return self.lines.get_line(self.current - 1) + '\n'
99
return ''
100
101
def __call__(self):
102
return self.readline()
103
104
105
class _CustomGenerator(object):
106
107
def __init__(self, lines):
108
self.lines = lines
109
self.in_string = ''
110
self.open_count = 0
111
self.continuation = False
112
113
def __call__(self):
114
size = self.lines.length()
115
result = []
116
i = 1
117
while i <= size:
118
while i <= size and not self.lines.get_line(i).strip():
119
i += 1
120
if i <= size:
121
start = i
122
while True:
123
line = self.lines.get_line(i)
124
self._analyze_line(line)
125
if not (self.continuation or self.open_count or
126
self.in_string) or i == size:
127
break
128
i += 1
129
result.append((start, i))
130
i += 1
131
return result
132
133
_main_chars = re.compile(r'[\'|"|#|\\|\[|\]|\{|\}|\(|\)]')
134
def _analyze_line(self, line):
135
char = None
136
for match in self._main_chars.finditer(line):
137
char = match.group()
138
i = match.start()
139
if char in '\'"':
140
if not self.in_string:
141
self.in_string = char
142
if char * 3 == line[i:i + 3]:
143
self.in_string = char * 3
144
elif self.in_string == line[i:i + len(self.in_string)] and \
145
not (i > 0 and line[i - 1] == '\\' and
146
not (i > 1 and line[i - 2] == '\\')):
147
self.in_string = ''
148
if self.in_string:
149
continue
150
if char == '#':
151
break
152
if char in '([{':
153
self.open_count += 1
154
elif char in ')]}':
155
self.open_count -= 1
156
if line and char != '#' and line.endswith('\\'):
157
self.continuation = True
158
else:
159
self.continuation = False
160
161
def custom_generator(lines):
162
return _CustomGenerator(lines)()
163
164
165
class LogicalLineFinder(object):
166
167
def __init__(self, lines):
168
self.lines = lines
169
170
def logical_line_in(self, line_number):
171
indents = count_line_indents(self.lines.get_line(line_number))
172
tries = 0
173
while True:
174
block_start = get_block_start(self.lines, line_number, indents)
175
try:
176
return self._block_logical_line(block_start, line_number)
177
except IndentationError, e:
178
tries += 1
179
if tries == 5:
180
raise e
181
lineno = e.lineno + block_start - 1
182
indents = count_line_indents(self.lines.get_line(lineno))
183
184
def generate_starts(self, start_line=1, end_line=None):
185
for start, end in self.generate_regions(start_line, end_line):
186
yield start
187
188
def generate_regions(self, start_line=1, end_line=None):
189
# XXX: `block_start` should be at a better position!
190
block_start = 1
191
readline = LinesToReadline(self.lines, block_start)
192
shifted = start_line - block_start + 1
193
try:
194
for start, end in self._logical_lines(readline):
195
real_start = start + block_start - 1
196
real_start = self._first_non_blank(real_start)
197
if end_line is not None and real_start >= end_line:
198
break
199
real_end = end + block_start - 1
200
if real_start >= start_line:
201
yield (real_start, real_end)
202
except tokenize.TokenError, e:
203
pass
204
205
def _block_logical_line(self, block_start, line_number):
206
readline = LinesToReadline(self.lines, block_start)
207
shifted = line_number - block_start + 1
208
region = self._calculate_logical(readline, shifted)
209
start = self._first_non_blank(region[0] + block_start - 1)
210
if region[1] is None:
211
end = self.lines.length()
212
else:
213
end = region[1] + block_start - 1
214
return start, end
215
216
def _calculate_logical(self, readline, line_number):
217
last_end = 1
218
try:
219
for start, end in self._logical_lines(readline):
220
if line_number <= end:
221
return (start, end)
222
last_end = end + 1
223
except tokenize.TokenError, e:
224
current = e.args[1][0]
225
return (last_end, max(last_end, current - 1))
226
return (last_end, None)
227
228
def _logical_lines(self, readline):
229
last_end = 1
230
for current_token in tokenize.generate_tokens(readline):
231
current = current_token[2][0]
232
if current_token[0] == token.NEWLINE:
233
yield (last_end, current)
234
last_end = current + 1
235
236
def _first_non_blank(self, line_number):
237
current = line_number
238
while current < self.lines.length():
239
line = self.lines.get_line(current).strip()
240
if line and not line.startswith('#'):
241
return current
242
current += 1
243
return current
244
245
246
def tokenizer_generator(lines):
247
return LogicalLineFinder(lines).generate_regions()
248
249
250
class CachingLogicalLineFinder(object):
251
252
def __init__(self, lines, generate=custom_generator):
253
self.lines = lines
254
self._generate = generate
255
256
_starts = None
257
@property
258
def starts(self):
259
if self._starts is None:
260
self._init_logicals()
261
return self._starts
262
263
_ends = None
264
@property
265
def ends(self):
266
if self._ends is None:
267
self._init_logicals()
268
return self._ends
269
270
def _init_logicals(self):
271
"""Should initialize _starts and _ends attributes"""
272
size = self.lines.length() + 1
273
self._starts = [None] * size
274
self._ends = [None] * size
275
for start, end in self._generate(self.lines):
276
self._starts[start] = True
277
self._ends[end] = True
278
279
def logical_line_in(self, line_number):
280
start = line_number
281
while start > 0 and not self.starts[start]:
282
start -= 1
283
if start == 0:
284
try:
285
start = self.starts.index(True, line_number)
286
except ValueError:
287
return (line_number, line_number)
288
return (start, self.ends.index(True, start))
289
290
def generate_starts(self, start_line=1, end_line=None):
291
if end_line is None:
292
end_line = self.lines.length()
293
for index in range(start_line, end_line):
294
if self.starts[index]:
295
yield index
296
297
298
def get_block_start(lines, lineno, maximum_indents=80):
299
"""Approximate block start"""
300
pattern = get_block_start_patterns()
301
for i in range(lineno, 0, -1):
302
match = pattern.search(lines.get_line(i))
303
if match is not None and \
304
count_line_indents(lines.get_line(i)) <= maximum_indents:
305
striped = match.string.lstrip()
306
# Maybe we're in a list comprehension or generator expression
307
if i > 1 and striped.startswith('if') or striped.startswith('for'):
308
bracs = 0
309
for j in range(i, min(i + 5, lines.length() + 1)):
310
for c in lines.get_line(j):
311
if c == '#':
312
break
313
if c in '[(':
314
bracs += 1
315
if c in ')]':
316
bracs -= 1
317
if bracs < 0:
318
break
319
if bracs < 0:
320
break
321
if bracs < 0:
322
continue
323
return i
324
return 1
325
326
327
_block_start_pattern = None
328
329
def get_block_start_patterns():
330
global _block_start_pattern
331
if not _block_start_pattern:
332
pattern = '^\\s*(((def|class|if|elif|except|for|while|with)\\s)|'\
333
'((try|else|finally|except)\\s*:))'
334
_block_start_pattern = re.compile(pattern, re.M)
335
return _block_start_pattern
336
337
338
def count_line_indents(line):
339
indents = 0
340
for char in line:
341
if char == ' ':
342
indents += 1
343
elif char == '\t':
344
indents += 8
345
else:
346
return indents
347
return 0
348
349
350
def get_string_pattern():
351
start = r'(\b[uU]?[rR]?)?'
352
longstr = r'%s"""(\\.|"(?!"")|\\\n|[^"\\])*"""' % start
353
shortstr = r'%s"(\\.|[^"\\\n])*"' % start
354
return '|'.join([longstr, longstr.replace('"', "'"),
355
shortstr, shortstr.replace('"', "'")])
356
357
def get_comment_pattern():
358
return r'#[^\n]*'
359
360