Skip to content

Commit 6c61c66

Browse files
authored
[html-report] Display unreachable code as Any (#3616)
Fixes #2785
1 parent 0d045cd commit 6c61c66

File tree

7 files changed

+282
-88
lines changed

7 files changed

+282
-88
lines changed

mypy/report.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""Classes for producing HTML reports about imprecision."""
22

33
from abc import ABCMeta, abstractmethod
4-
import cgi
54
import json
65
import os
76
import shutil
87
import tokenize
98
from operator import attrgetter
10-
9+
from urllib.request import pathname2url
1110
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
1211

1312
import time
@@ -157,9 +156,12 @@ def on_file(self,
157156
visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(),
158157
typemap=type_map, all_nodes=True)
159158
tree.accept(visitor)
160-
num_total = visitor.num_imprecise + visitor.num_precise + visitor.num_any
159+
num_unanalyzed_lines = list(visitor.line_map.values()).count(stats.TYPE_UNANALYZED)
160+
# count each line of dead code as one expression of type "Any"
161+
num_any = visitor.num_any + num_unanalyzed_lines
162+
num_total = visitor.num_imprecise + visitor.num_precise + num_any
161163
if num_total > 0:
162-
self.counts[tree.fullname()] = (visitor.num_any, num_total)
164+
self.counts[tree.fullname()] = (num_any, num_total)
163165

164166
def on_finish(self) -> None:
165167
total_any = sum(num_any for num_any, _ in self.counts.values())
@@ -171,9 +173,11 @@ def on_finish(self) -> None:
171173
any_column_name = "Anys"
172174
total_column_name = "Exprs"
173175
file_column_name = "Name"
176+
total_row_name = "Total"
174177
coverage_column_name = "Coverage"
175178
# find the longest filename all files
176-
name_width = max([len(file) for file in self.counts] + [len(file_column_name)])
179+
name_width = max([len(file) for file in self.counts] +
180+
[len(file_column_name), len(total_row_name)])
177181
# totals are the largest numbers in their column - no need to look at others
178182
min_column_distance = 3 # minimum distance between numbers in two columns
179183
any_width = max(len(str(total_any)) + min_column_distance, len(any_column_name))
@@ -199,8 +203,8 @@ def on_finish(self) -> None:
199203
coverage_width=coverage_width))
200204
f.write(separator)
201205
f.write('{:{name_width}} {:{any_width}} {:{total_width}} {:>{coverage_width}.2f}%\n'
202-
.format('Total', total_any, total_expr, total_coverage, name_width=name_width,
203-
any_width=any_width, total_width=exprs_width,
206+
.format(total_row_name, total_any, total_expr, total_coverage,
207+
name_width=name_width, any_width=any_width, total_width=exprs_width,
204208
coverage_width=coverage_width))
205209

206210

@@ -402,7 +406,7 @@ def on_file(self,
402406
# Assumes a layout similar to what XmlReporter uses.
403407
xslt_path = os.path.relpath('mypy-html.xslt', path)
404408
transform_pi = etree.ProcessingInstruction('xml-stylesheet',
405-
'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
409+
'type="text/xsl" href="%s"' % pathname2url(xslt_path))
406410
root.addprevious(transform_pi)
407411
self.schema.assertValid(doc)
408412

@@ -425,7 +429,7 @@ def on_finish(self) -> None:
425429
module=file_info.module)
426430
xslt_path = os.path.relpath('mypy-html.xslt', '.')
427431
transform_pi = etree.ProcessingInstruction('xml-stylesheet',
428-
'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
432+
'type="text/xsl" href="%s"' % pathname2url(xslt_path))
429433
root.addprevious(transform_pi)
430434
self.schema.assertValid(doc)
431435

mypy/stats.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import cgi
44
import os.path
55

6-
from typing import Any, Dict, List, cast, Tuple
6+
from typing import Dict, List, cast, Tuple, Set, Optional
77

88
from mypy.traverser import TraverserVisitor
99
from mypy.types import (
@@ -12,17 +12,19 @@
1212
from mypy import nodes
1313
from mypy.nodes import (
1414
Expression, FuncDef, TypeApplication, AssignmentStmt, NameExpr, CallExpr, MypyFile,
15-
MemberExpr, OpExpr, ComparisonExpr, IndexExpr, UnaryExpr, YieldFromExpr, RefExpr
15+
MemberExpr, OpExpr, ComparisonExpr, IndexExpr, UnaryExpr, YieldFromExpr, RefExpr, ClassDef
1616
)
1717

1818

1919
TYPE_EMPTY = 0
20-
TYPE_PRECISE = 1
21-
TYPE_IMPRECISE = 2
22-
TYPE_ANY = 3
20+
TYPE_UNANALYZED = 1 # type of non-typechecked code
21+
TYPE_PRECISE = 2
22+
TYPE_IMPRECISE = 3
23+
TYPE_ANY = 4
2324

2425
precision_names = [
2526
'empty',
27+
'unanalyzed',
2628
'precise',
2729
'imprecise',
2830
'any',
@@ -79,6 +81,15 @@ def visit_func_def(self, o: FuncDef) -> None:
7981
self.record_line(self.line, TYPE_ANY)
8082
super().visit_func_def(o)
8183

84+
def visit_class_def(self, o: ClassDef) -> None:
85+
# Override this method because we don't want to analyze base_type_exprs (base_type_exprs
86+
# are base classes in a class declaration).
87+
# While base_type_exprs are technically expressions, type analyzer does not visit them and
88+
# they are not in the typemap.
89+
for d in o.decorators:
90+
d.accept(self)
91+
o.defs.accept(self)
92+
8293
def visit_type_application(self, o: TypeApplication) -> None:
8394
self.line = o.line
8495
for t in o.types:
@@ -155,12 +166,17 @@ def visit_unary_expr(self, o: UnaryExpr) -> None:
155166
def process_node(self, node: Expression) -> None:
156167
if self.all_nodes:
157168
if self.typemap is not None:
158-
typ = self.typemap.get(node)
159-
if typ:
160-
self.line = node.line
161-
self.type(typ)
169+
self.line = node.line
170+
self.type(self.typemap.get(node))
171+
172+
def type(self, t: Optional[Type]) -> None:
173+
if not t:
174+
# If an expression does not have a type, it is often due to dead code.
175+
# Don't count these because there can be an unanalyzed value on a line with other
176+
# analyzed expressions, which overwrite the TYPE_UNANALYZED.
177+
self.record_line(self.line, TYPE_UNANALYZED)
178+
return
162179

163-
def type(self, t: Type) -> None:
164180
if isinstance(t, AnyType):
165181
self.log(' !! Any type around line %d' % self.line)
166182
self.num_any += 1
@@ -197,7 +213,7 @@ def log(self, string: str) -> None:
197213

198214
def record_line(self, line: int, precision: int) -> None:
199215
self.line_map[line] = max(precision,
200-
self.line_map.get(line, TYPE_PRECISE))
216+
self.line_map.get(line, TYPE_EMPTY))
201217

202218

203219
def dump_type_stats(tree: MypyFile, path: str, inferred: bool = False,

mypy/test/testcmdline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
python3_path = sys.executable
2323

2424
# Files containing test case descriptions.
25-
cmdline_files = ['cmdline.test']
25+
cmdline_files = [
26+
'cmdline.test',
27+
'reports.test',
28+
]
2629

2730

2831
class PythonEvaluationSuite(Suite):

test-data/unit/cmdline.test

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,6 @@ bad = 0
304304
[out]
305305
mypy.ini: [mypy]: Unrecognized option: bad = 0
306306

307-
[case testConfigErrorUnknownReport]
308-
# cmd: mypy -c pass
309-
[file mypy.ini]
310-
[[mypy]
311-
bad_report = .
312-
[out]
313-
mypy.ini: [mypy]: Unrecognized report type: bad_report
314-
315307
[case testConfigErrorBadBoolean]
316308
# cmd: mypy -c pass
317309
[file mypy.ini]
@@ -329,64 +321,6 @@ python_version = 3.4
329321
[out]
330322
mypy.ini: [mypy-*]: Per-module sections should only specify per-module flags (python_version)
331323

332-
[case testAnyExprReportDivisionByZero]
333-
# cmd: mypy --any-exprs-report=out -c 'pass'
334-
335-
[case testCoberturaParser]
336-
# cmd: mypy --cobertura-xml-report build pkg
337-
[file pkg/__init__.py]
338-
[file pkg/a.py]
339-
from typing import Dict
340-
341-
def foo() -> Dict:
342-
z = {'hello': 'world'}
343-
return z
344-
[file pkg/subpkg/__init__.py]
345-
[file pkg/subpkg/a.py]
346-
def bar() -> str:
347-
return 'world'
348-
def untyped_function():
349-
return 42
350-
[outfile build/cobertura.xml]
351-
<coverage timestamp="$TIMESTAMP" version="$VERSION" line-rate="0.8000" branch-rate="0">
352-
<sources>
353-
<source>$PWD</source>
354-
</sources>
355-
<packages>
356-
<package complexity="1.0" name="pkg" branch-rate="0" line-rate="1.0000">
357-
<classes>
358-
<class complexity="1.0" filename="pkg/__init__.py" name="__init__.py" branch-rate="0" line-rate="1.0">
359-
<methods/>
360-
<lines/>
361-
</class>
362-
<class complexity="1.0" filename="pkg/a.py" name="a.py" branch-rate="0" line-rate="1.0000">
363-
<methods/>
364-
<lines>
365-
<line branch="true" hits="1" number="3" precision="imprecise" condition-coverage="50% (1/2)"/>
366-
<line branch="false" hits="1" number="4" precision="precise"/>
367-
<line branch="false" hits="1" number="5" precision="precise"/>
368-
</lines>
369-
</class>
370-
</classes>
371-
</package>
372-
<package complexity="1.0" name="pkg.subpkg" branch-rate="0" line-rate="0.5000">
373-
<classes>
374-
<class complexity="1.0" filename="pkg/subpkg/__init__.py" name="__init__.py" branch-rate="0" line-rate="1.0">
375-
<methods/>
376-
<lines/>
377-
</class>
378-
<class complexity="1.0" filename="pkg/subpkg/a.py" name="a.py" branch-rate="0" line-rate="0.5000">
379-
<methods/>
380-
<lines>
381-
<line branch="false" hits="1" number="1" precision="precise"/>
382-
<line branch="false" hits="0" number="3" precision="any"/>
383-
</lines>
384-
</class>
385-
</classes>
386-
</package>
387-
</packages>
388-
</coverage>
389-
390324
[case testConfigMypyPath]
391325
# cmd: mypy file.py
392326
[file mypy.ini]

0 commit comments

Comments
 (0)