Skip to content

Commit fde83d5

Browse files
bavardagegvanrossum
authored andcommitted
Add column number support (#2163)
When mypy is run with `--show-column-numbers`, error messages include columns. Column numbers are 0-based. Fixes #1216.
1 parent 8f0316f commit fde83d5

21 files changed

+335
-173
lines changed

mypy/build.py

+19-16
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def __init__(self, data_dir: str,
343343
version_id: str) -> None:
344344
self.start_time = time.time()
345345
self.data_dir = data_dir
346-
self.errors = Errors(options.suppress_error_context)
346+
self.errors = Errors(options.suppress_error_context, options.show_column_numbers)
347347
self.errors.set_ignore_prefix(ignore_prefix)
348348
self.lib_path = tuple(lib_path)
349349
self.source_set = source_set
@@ -454,15 +454,15 @@ def module_not_found(self, path: str, line: int, id: str) -> None:
454454
if ((self.options.python_version[0] == 2 and moduleinfo.is_py2_std_lib_module(id)) or
455455
(self.options.python_version[0] >= 3 and moduleinfo.is_py3_std_lib_module(id))):
456456
self.errors.report(
457-
line, "No library stub file for standard library module '{}'".format(id))
458-
self.errors.report(line, stub_msg, severity='note', only_once=True)
457+
line, 0, "No library stub file for standard library module '{}'".format(id))
458+
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
459459
elif moduleinfo.is_third_party_module(id):
460-
self.errors.report(line, "No library stub file for module '{}'".format(id))
461-
self.errors.report(line, stub_msg, severity='note', only_once=True)
460+
self.errors.report(line, 0, "No library stub file for module '{}'".format(id))
461+
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
462462
else:
463-
self.errors.report(line, "Cannot find module named '{}'".format(id))
464-
self.errors.report(line, '(Perhaps setting MYPYPATH '
465-
'or using the "--silent-imports" flag would help)',
463+
self.errors.report(line, 0, "Cannot find module named '{}'".format(id))
464+
self.errors.report(line, 0, '(Perhaps setting MYPYPATH '
465+
'or using the "--silent-imports" flag would help)',
466466
severity='note', only_once=True)
467467

468468
def report_file(self, file: MypyFile) -> None:
@@ -1190,11 +1190,11 @@ def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None:
11901190
manager = self.manager
11911191
manager.errors.set_import_context([])
11921192
manager.errors.set_file(ancestor_for.xpath)
1193-
manager.errors.report(-1, "Ancestor package '%s' silently ignored" % (id,),
1193+
manager.errors.report(-1, -1, "Ancestor package '%s' silently ignored" % (id,),
11941194
severity='note', only_once=True)
1195-
manager.errors.report(-1, "(Using --silent-imports, submodule passed on command line)",
1195+
manager.errors.report(-1, -1, "(Using --silent-imports, submodule passed on command line)",
11961196
severity='note', only_once=True)
1197-
manager.errors.report(-1, "(This note brought to you by --almost-silent)",
1197+
manager.errors.report(-1, -1, "(This note brought to you by --almost-silent)",
11981198
severity='note', only_once=True)
11991199

12001200
def skipping_module(self, id: str, path: str) -> None:
@@ -1204,11 +1204,13 @@ def skipping_module(self, id: str, path: str) -> None:
12041204
manager.errors.set_import_context(self.caller_state.import_context)
12051205
manager.errors.set_file(self.caller_state.xpath)
12061206
line = self.caller_line
1207-
manager.errors.report(line, "Import of '%s' silently ignored" % (id,),
1207+
manager.errors.report(line, 0,
1208+
"Import of '%s' silently ignored" % (id,),
12081209
severity='note')
1209-
manager.errors.report(line, "(Using --silent-imports, module not passed on command line)",
1210+
manager.errors.report(line, 0,
1211+
"(Using --silent-imports, module not passed on command line)",
12101212
severity='note', only_once=True)
1211-
manager.errors.report(line, "(This note courtesy of --almost-silent)",
1213+
manager.errors.report(line, 0, "(This note courtesy of --almost-silent)",
12121214
severity='note', only_once=True)
12131215
manager.errors.set_import_context(save_import_context)
12141216

@@ -1378,7 +1380,8 @@ def parse_file(self) -> None:
13781380
if id == '':
13791381
# Must be from a relative import.
13801382
manager.errors.set_file(self.xpath)
1381-
manager.errors.report(line, "No parent module -- cannot perform relative import",
1383+
manager.errors.report(line, 0,
1384+
"No parent module -- cannot perform relative import",
13821385
blocker=True)
13831386
continue
13841387
if id not in dep_line_map:
@@ -1492,7 +1495,7 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:
14921495
continue
14931496
if st.id in graph:
14941497
manager.errors.set_file(st.xpath)
1495-
manager.errors.report(-1, "Duplicate module named '%s'" % st.id)
1498+
manager.errors.report(-1, -1, "Duplicate module named '%s'" % st.id)
14961499
manager.errors.raise_error()
14971500
graph[st.id] = st
14981501
new.append(st)

mypy/errors.py

+40-26
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class ErrorInfo:
2929
# The line number related to this error within file.
3030
line = 0 # -1 if unknown
3131

32+
# The column number related to this error with file.
33+
column = 0 # -1 if unknown
34+
3235
# Either 'error' or 'note'.
3336
severity = ''
3437

@@ -42,13 +45,14 @@ class ErrorInfo:
4245
only_once = False
4346

4447
def __init__(self, import_ctx: List[Tuple[str, int]], file: str, typ: str,
45-
function_or_member: str, line: int, severity: str, message: str,
46-
blocker: bool, only_once: bool) -> None:
48+
function_or_member: str, line: int, column: int, severity: str,
49+
message: str, blocker: bool, only_once: bool) -> None:
4750
self.import_ctx = import_ctx
4851
self.file = file
4952
self.type = typ
5053
self.function_or_member = function_or_member
5154
self.line = line
55+
self.column = column
5256
self.severity = severity
5357
self.message = message
5458
self.blocker = blocker
@@ -92,7 +96,11 @@ class Errors:
9296
# Set to True to suppress "In function "foo":" messages.
9397
suppress_error_context = False # type: bool
9498

95-
def __init__(self, suppress_error_context: bool = False) -> None:
99+
# Set to True to show column numbers in error messages
100+
show_column_numbers = False # type: bool
101+
102+
def __init__(self, suppress_error_context: bool = False,
103+
show_column_numbers: bool = False) -> None:
96104
self.error_info = []
97105
self.import_ctx = []
98106
self.type_name = [None]
@@ -101,9 +109,10 @@ def __init__(self, suppress_error_context: bool = False) -> None:
101109
self.used_ignored_lines = defaultdict(set)
102110
self.only_once_messages = set()
103111
self.suppress_error_context = suppress_error_context
112+
self.show_column_numbers = show_column_numbers
104113

105114
def copy(self) -> 'Errors':
106-
new = Errors(self.suppress_error_context)
115+
new = Errors(self.suppress_error_context, self.show_column_numbers)
107116
new.file = self.file
108117
new.import_ctx = self.import_ctx[:]
109118
new.type_name = self.type_name[:]
@@ -169,7 +178,7 @@ def set_import_context(self, ctx: List[Tuple[str, int]]) -> None:
169178
"""Replace the entire import context with a new value."""
170179
self.import_ctx = ctx[:]
171180

172-
def report(self, line: int, message: str, blocker: bool = False,
181+
def report(self, line: int, column: int, message: str, blocker: bool = False,
173182
severity: str = 'error', file: str = None, only_once: bool = False) -> None:
174183
"""Report message at the given line using the current error context.
175184
@@ -187,7 +196,7 @@ def report(self, line: int, message: str, blocker: bool = False,
187196
if file is None:
188197
file = self.file
189198
info = ErrorInfo(self.import_context(), file, type,
190-
self.function_or_member[-1], line, severity, message,
199+
self.function_or_member[-1], line, column, severity, message,
191200
blocker, only_once)
192201
self.add_error_info(info)
193202

@@ -210,7 +219,7 @@ def generate_unused_ignore_notes(self) -> None:
210219
for line in ignored_lines - self.used_ignored_lines[file]:
211220
# Don't use report since add_error_info will ignore the error!
212221
info = ErrorInfo(self.import_context(), file, None, None,
213-
line, 'note', "unused 'type: ignore' comment",
222+
line, -1, 'note', "unused 'type: ignore' comment",
214223
False, False)
215224
self.error_info.append(info)
216225

@@ -245,10 +254,13 @@ def messages(self) -> List[str]:
245254
a = [] # type: List[str]
246255
errors = self.render_messages(self.sort_messages(self.error_info))
247256
errors = self.remove_duplicates(errors)
248-
for file, line, severity, message in errors:
257+
for file, line, column, severity, message in errors:
249258
s = ''
250259
if file is not None:
251-
if line is not None and line >= 0:
260+
if self.show_column_numbers and line is not None and line >= 0 \
261+
and column is not None and column >= 0:
262+
srcloc = '{}:{}:{}'.format(file, line, column)
263+
elif line is not None and line >= 0:
252264
srcloc = '{}:{}'.format(file, line)
253265
else:
254266
srcloc = file
@@ -258,16 +270,17 @@ def messages(self) -> List[str]:
258270
a.append(s)
259271
return a
260272

261-
def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
273+
def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int, int,
262274
str, str]]:
263275
"""Translate the messages into a sequence of tuples.
264276
265-
Each tuple is of form (path, line, message. The rendered
277+
Each tuple is of form (path, line, col, message. The rendered
266278
sequence includes information about error contexts. The path
267279
item may be None. If the line item is negative, the line
268280
number is not defined for the tuple.
269281
"""
270-
result = [] # type: List[Tuple[str, int, str, str]] # (path, line, severity, message)
282+
result = [] # type: List[Tuple[str, int, int, str, str]]
283+
# (path, line, column, severity, message)
271284

272285
prev_import_context = [] # type: List[Tuple[str, int]]
273286
prev_function_or_member = None # type: str
@@ -290,7 +303,7 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
290303
# Remove prefix to ignore from path (if present) to
291304
# simplify path.
292305
path = remove_path_prefix(path, self.ignore_prefix)
293-
result.append((None, -1, 'note', fmt.format(path, line)))
306+
result.append((None, -1, -1, 'note', fmt.format(path, line)))
294307
i -= 1
295308

296309
file = self.simplify_path(e.file)
@@ -302,27 +315,27 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int,
302315
e.type != prev_type):
303316
if e.function_or_member is None:
304317
if e.type is None:
305-
result.append((file, -1, 'note', 'At top level:'))
318+
result.append((file, -1, -1, 'note', 'At top level:'))
306319
else:
307-
result.append((file, -1, 'note', 'In class "{}":'.format(
320+
result.append((file, -1, -1, 'note', 'In class "{}":'.format(
308321
e.type)))
309322
else:
310323
if e.type is None:
311-
result.append((file, -1, 'note',
324+
result.append((file, -1, -1, 'note',
312325
'In function "{}":'.format(
313326
e.function_or_member)))
314327
else:
315-
result.append((file, -1, 'note',
328+
result.append((file, -1, -1, 'note',
316329
'In member "{}" of class "{}":'.format(
317330
e.function_or_member, e.type)))
318331
elif e.type != prev_type:
319332
if e.type is None:
320-
result.append((file, -1, 'note', 'At top level:'))
333+
result.append((file, -1, -1, 'note', 'At top level:'))
321334
else:
322-
result.append((file, -1, 'note',
335+
result.append((file, -1, -1, 'note',
323336
'In class "{}":'.format(e.type)))
324337

325-
result.append((file, e.line, e.severity, e.message))
338+
result.append((file, e.line, e.column, e.severity, e.message))
326339

327340
prev_import_context = e.import_ctx
328341
prev_function_or_member = e.function_or_member
@@ -348,22 +361,23 @@ def sort_messages(self, errors: List[ErrorInfo]) -> List[ErrorInfo]:
348361
i += 1
349362
i += 1
350363

351-
# Sort the errors specific to a file according to line number.
352-
a = sorted(errors[i0:i], key=lambda x: x.line)
364+
# Sort the errors specific to a file according to line number and column.
365+
a = sorted(errors[i0:i], key=lambda x: (x.line, x.column))
353366
result.extend(a)
354367
return result
355368

356-
def remove_duplicates(self, errors: List[Tuple[str, int, str, str]]
357-
) -> List[Tuple[str, int, str, str]]:
369+
def remove_duplicates(self, errors: List[Tuple[str, int, int, str, str]]
370+
) -> List[Tuple[str, int, int, str, str]]:
358371
"""Remove duplicates from a sorted error list."""
359-
res = [] # type: List[Tuple[str, int, str, str]]
372+
res = [] # type: List[Tuple[str, int, int, str, str]]
360373
i = 0
361374
while i < len(errors):
362375
dup = False
363376
j = i - 1
364377
while (j >= 0 and errors[j][0] == errors[i][0] and
365378
errors[j][1] == errors[i][1]):
366-
if errors[j] == errors[i]:
379+
if (errors[j][3] == errors[i][3] and
380+
errors[j][4] == errors[i][4]): # ignore column
367381
dup = True
368382
break
369383
j -= 1

mypy/expandtype.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,15 @@ def visit_erased_type(self, t: ErasedType) -> Type:
6666

6767
def visit_instance(self, t: Instance) -> Type:
6868
args = self.expand_types(t.args)
69-
return Instance(t.type, args, t.line)
69+
return Instance(t.type, args, t.line, t.column)
7070

7171
def visit_type_var(self, t: TypeVarType) -> Type:
7272
repl = self.variables.get(t.id, t)
7373
if isinstance(repl, Instance):
7474
inst = repl
7575
# Return copy of instance with type erasure flag on.
76-
return Instance(inst.type, inst.args, inst.line, True)
76+
return Instance(inst.type, inst.args, line=inst.line,
77+
column=inst.column, erased=True)
7778
else:
7879
return repl
7980

@@ -93,7 +94,7 @@ def visit_tuple_type(self, t: TupleType) -> Type:
9394
def visit_union_type(self, t: UnionType) -> Type:
9495
# After substituting for type variables in t.items,
9596
# some of the resulting types might be subtypes of others.
96-
return UnionType.make_simplified_union(self.expand_types(t.items), t.line)
97+
return UnionType.make_simplified_union(self.expand_types(t.items), t.line, t.column)
9798

9899
def visit_partial_type(self, t: PartialType) -> Type:
99100
return t

mypy/fastparse.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
7070
except (SyntaxError, TypeCommentParseError) as e:
7171
if errors:
7272
errors.set_file('<input>' if fnam is None else fnam)
73-
errors.report(e.lineno, e.msg)
73+
errors.report(e.lineno, e.offset, e.msg)
7474
else:
7575
raise
7676

@@ -84,8 +84,8 @@ def parse(source: Union[str, bytes], fnam: str = None, errors: Errors = None,
8484
def parse_type_comment(type_comment: str, line: int) -> Type:
8585
try:
8686
typ = ast35.parse(type_comment, '<type_comment>', 'eval')
87-
except SyntaxError:
88-
raise TypeCommentParseError(TYPE_COMMENT_SYNTAX_ERROR, line)
87+
except SyntaxError as e:
88+
raise TypeCommentParseError(TYPE_COMMENT_SYNTAX_ERROR, line, e.offset)
8989
else:
9090
assert isinstance(typ, ast35.Expression)
9191
return TypeConverter(line=line).visit(typ.body)
@@ -95,7 +95,7 @@ def with_line(f: Callable[['ASTConverter', T], U]) -> Callable[['ASTConverter',
9595
@wraps(f)
9696
def wrapper(self: 'ASTConverter', ast: T) -> U:
9797
node = f(self, ast)
98-
node.set_line(ast.lineno)
98+
node.set_line(ast.lineno, ast.col_offset)
9999
return node
100100
return wrapper
101101

@@ -260,7 +260,7 @@ def do_func_def(self, n: Union[ast35.FunctionDef, ast35.AsyncFunctionDef],
260260
try:
261261
func_type_ast = ast35.parse(n.type_comment, '<func_type>', 'func_type')
262262
except SyntaxError:
263-
raise TypeCommentParseError(TYPE_COMMENT_SYNTAX_ERROR, n.lineno)
263+
raise TypeCommentParseError(TYPE_COMMENT_SYNTAX_ERROR, n.lineno, n.col_offset)
264264
assert isinstance(func_type_ast, ast35.FunctionType)
265265
# for ellipsis arg
266266
if (len(func_type_ast.argtypes) == 1 and
@@ -600,6 +600,7 @@ def visit_UnaryOp(self, n: ast35.UnaryOp) -> Node:
600600
def visit_Lambda(self, n: ast35.Lambda) -> Node:
601601
body = ast35.Return(n.body)
602602
body.lineno = n.lineno
603+
body.col_offset = n.col_offset
603604

604605
return FuncExpr(self.transform_args(n.args, n.lineno),
605606
self.as_block([body], n.lineno))
@@ -804,7 +805,8 @@ def visit_raw_str(self, s: str) -> Type:
804805
return parse_type_comment(s.strip(), line=self.line)
805806

806807
def generic_visit(self, node: ast35.AST) -> None:
807-
raise TypeCommentParseError(TYPE_COMMENT_AST_ERROR, self.line)
808+
raise TypeCommentParseError(TYPE_COMMENT_AST_ERROR, self.line,
809+
getattr(node, 'col_offset', -1))
808810

809811
def visit_NoneType(self, n: Any) -> Type:
810812
return None
@@ -860,6 +862,7 @@ def visit_List(self, n: ast35.List) -> Type:
860862

861863

862864
class TypeCommentParseError(Exception):
863-
def __init__(self, msg: str, lineno: int) -> None:
865+
def __init__(self, msg: str, lineno: int, offset: int) -> None:
864866
self.msg = msg
865867
self.lineno = lineno
868+
self.offset = offset

0 commit comments

Comments
 (0)