Skip to content

Commit 0f18d8f

Browse files
committed
Add HTML report type with tooltips
This adds a HTML reporter that will produce mouse-over type tooltips in the output. The command line argument is --xslt-html-tooltips-report. This also adds by-default-disabled functionality to stats.StatisticsVisitor to build a detailed map of the form (Line, Column) -> TypeString. It also extends the report.MemoryXmlReporter to optionally add XML elements from the extended StatisticsVisitor.
1 parent 9f3cbcd commit 0f18d8f

File tree

5 files changed

+194
-4
lines changed

5 files changed

+194
-4
lines changed

mypy/report.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,18 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
285285
super().__init__(reports, output_dir)
286286

287287
self.xslt_html_path = os.path.join(reports.data_dir, 'xml', 'mypy-html.xslt')
288+
self.xslt_html_tooltips_path = os.path.join(
289+
reports.data_dir, 'xml', 'mypy-html-tooltips.xslt')
288290
self.xslt_txt_path = os.path.join(reports.data_dir, 'xml', 'mypy-txt.xslt')
289291
self.css_html_path = os.path.join(reports.data_dir, 'xml', 'mypy-html.css')
290292
xsd_path = os.path.join(reports.data_dir, 'xml', 'mypy.xsd')
291293
self.schema = etree.XMLSchema(etree.parse(xsd_path))
292294
self.last_xml = None # type: etree._ElementTree
293295
self.files = [] # type: List[FileInfo]
296+
self.type_columns = False
297+
298+
def set_type_columns(self, type_columns: bool) -> None:
299+
self.type_columns = type_columns
294300

295301
def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type]) -> None:
296302
self.last_xml = None
@@ -302,7 +308,8 @@ def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type]) -> None:
302308
if 'stubs' in path.split('/'):
303309
return
304310

305-
visitor = stats.StatisticsVisitor(inferred=True, typemap=type_map, all_nodes=True)
311+
visitor = stats.StatisticsVisitor(inferred=True, typemap=type_map, all_nodes=True,
312+
linecol_typestr=self.type_columns)
306313
tree.accept(visitor)
307314

308315
root = etree.Element('mypy-report-file', name=path, module=tree._fullname)
@@ -313,10 +320,28 @@ def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type]) -> None:
313320
for lineno, line_text in enumerate(input_file, 1):
314321
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
315322
file_info.counts[status] += 1
316-
etree.SubElement(root, 'line',
323+
line = etree.SubElement(root, 'line',
317324
number=str(lineno),
318325
precision=stats.precision_names[status],
319326
content=line_text[:-1])
327+
328+
if self.type_columns:
329+
for column, ch in enumerate(line_text[:-1]):
330+
if lineno in visitor.linecol_typename_map:
331+
row = visitor.linecol_typename_map[lineno]
332+
if column in row:
333+
typestr = row[column]
334+
else:
335+
typestr = ""
336+
else:
337+
typestr = ""
338+
339+
etree.SubElement(line,
340+
"char",
341+
column=str(column),
342+
typestr=typestr,
343+
content=ch)
344+
320345
# Assumes a layout similar to what XmlReporter uses.
321346
xslt_path = os.path.relpath('mypy-html.xslt', path)
322347
transform_pi = etree.ProcessingInstruction('xml-stylesheet',
@@ -530,9 +555,12 @@ class XsltHtmlReporter(AbstractXmlReporter):
530555
def __init__(self, reports: Reports, output_dir: str) -> None:
531556
super().__init__(reports, output_dir)
532557

533-
self.xslt_html = etree.XSLT(etree.parse(self.memory_xml.xslt_html_path))
558+
self.xslt_html = etree.XSLT(etree.parse(self.xslt_file_to_use()))
534559
self.param_html = etree.XSLT.strparam('html')
535560

561+
def xslt_file_to_use(self) -> str:
562+
return self.memory_xml.xslt_html_path
563+
536564
def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type]) -> None:
537565
last_xml = self.memory_xml.last_xml
538566
if last_xml is None:
@@ -560,6 +588,19 @@ def on_finish(self) -> None:
560588
register_reporter('xslt-html', XsltHtmlReporter, needs_lxml=True)
561589

562590

591+
class XsltHtmlTooltipsReporter(XsltHtmlReporter):
592+
def __init__(self, reports: Reports, output_dir: str) -> None:
593+
super().__init__(reports, output_dir)
594+
self.memory_xml.set_type_columns(True)
595+
596+
def xslt_file_to_use(self) -> str:
597+
return self.memory_xml.xslt_html_tooltips_path
598+
599+
600+
register_reporter('xslt-html-tooltips',
601+
XsltHtmlTooltipsReporter, needs_lxml=True)
602+
603+
563604
class XsltTxtReporter(AbstractXmlReporter):
564605
"""Public reporter that exports TXT via XSLT.
565606

mypy/stats.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@
3232

3333
class StatisticsVisitor(TraverserVisitor):
3434
def __init__(self, inferred: bool, typemap: Dict[Expression, Type] = None,
35-
all_nodes: bool = False) -> None:
35+
all_nodes: bool = False,
36+
linecol_typestr: bool = False) -> None:
3637
self.inferred = inferred
3738
self.typemap = typemap
3839
self.all_nodes = all_nodes
40+
self.linecol_typestr = linecol_typestr
3941

4042
self.num_precise = 0
4143
self.num_imprecise = 0
@@ -52,6 +54,8 @@ def __init__(self, inferred: bool, typemap: Dict[Expression, Type] = None,
5254

5355
self.line_map = {} # type: Dict[int, int]
5456

57+
self.linecol_typename_map = {} # type: Dict[int, Dict[int, str]]
58+
5559
self.output = [] # type: List[str]
5660

5761
TraverserVisitor.__init__(self)
@@ -152,6 +156,11 @@ def process_node(self, node: Expression) -> None:
152156
if self.all_nodes:
153157
typ = self.typemap.get(node)
154158
if typ:
159+
if self.linecol_typestr:
160+
self.record_node_typestr(
161+
node.line,
162+
node.column,
163+
str(typ))
155164
self.line = node.line
156165
self.type(typ)
157166

@@ -196,6 +205,16 @@ def record_line(self, line: int, precision: int) -> None:
196205
self.line_map[line] = max(precision,
197206
self.line_map.get(line, TYPE_PRECISE))
198207

208+
def record_node_typestr(self,
209+
line: int,
210+
column: int,
211+
typestr: str) -> None:
212+
m = self.linecol_typename_map
213+
214+
m.setdefault(line, m.get(line, {}))
215+
216+
m[line][column] = typestr
217+
199218

200219
def dump_type_stats(tree: MypyFile, path: str, inferred: bool = False,
201220
typemap: Dict[Expression, Type] = None) -> None:

xml/mypy-html-tooltips.xslt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- vim: set sts=2 sw=2: -->
3+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
4+
<xsl:param name="ext" select="'xml'"/>
5+
<xsl:output method="html"/>
6+
<xsl:variable name="xml_stylesheet_pi" select="string(//processing-instruction('xml-stylesheet'))"/>
7+
<xsl:variable name="stylesheet_name" select="substring($xml_stylesheet_pi, 23, string-length($xml_stylesheet_pi) - 28)"/>
8+
<xsl:template match="/mypy-report-index">
9+
<xsl:text disable-output-escaping='yes'>&lt;!DOCTYPE html&gt;&#10;</xsl:text>
10+
<html>
11+
<head>
12+
<link rel="stylesheet" type="text/css" href="{$stylesheet_name}.css"/>
13+
</head>
14+
<body>
15+
<h1>Mypy Type Check Coverage Summary</h1>
16+
<table class="summary">
17+
<caption>Summary from <xsl:value-of select="@name"/></caption>
18+
<thead>
19+
<tr class="summary">
20+
<th class="summary">File</th>
21+
<th class="summary">Imprecision</th>
22+
<th class="summary">Lines</th>
23+
</tr>
24+
</thead>
25+
<tfoot>
26+
<xsl:variable name="bad_lines" select="sum(file/@imprecise|file/@any)"/>
27+
<xsl:variable name="total_lines" select="sum(file/@total)"/>
28+
<xsl:variable name="global_score" select="$bad_lines div ($total_lines + not(number($total_lines)))"/>
29+
<xsl:variable name="global_quality" select="string(number(number($global_score) &gt; 0.00) + number(number($global_score) &gt;= 0.20))"/>
30+
<tr class="summary summary-quality-{$global_quality}">
31+
<th class="summary summary-filename">Total</th>
32+
<th class="summary summary-precision"><xsl:value-of select="format-number($global_score, '0.00%')"/> imprecise</th>
33+
<th class="summary summary-lines"><xsl:value-of select="$total_lines"/> LOC</th>
34+
</tr>
35+
</tfoot>
36+
<tbody>
37+
<xsl:for-each select="file">
38+
<xsl:variable name="local_score" select="(@imprecise + @any) div (@total + not(number(@total)))"/>
39+
<xsl:variable name="local_quality" select="string(number(number($local_score) &gt; 0.00) + number(number($local_score) &gt;= 0.20))"/>
40+
<tr class="summary summary-quality-{$local_quality}">
41+
<td class="summary summary-filename"><a href="{$ext}/{@name}.{$ext}"><xsl:value-of select="@module"/></a></td>
42+
<td class="summary summary-precision"><xsl:value-of select="format-number($local_score, '0.00%')"/> imprecise</td>
43+
<td class="summary summary-lines"><xsl:value-of select="@total"/> LOC</td>
44+
</tr>
45+
</xsl:for-each>
46+
</tbody>
47+
</table>
48+
</body>
49+
</html>
50+
</xsl:template>
51+
<xsl:template match="/mypy-report-file">
52+
<xsl:text disable-output-escaping='yes'>&lt;!DOCTYPE html&gt;&#10;</xsl:text>
53+
<html>
54+
<head>
55+
<link rel="stylesheet" type="text/css" href="{$stylesheet_name}.css"/>
56+
</head>
57+
<body>
58+
<h2><xsl:value-of select="@module"/></h2>
59+
<table>
60+
<caption><xsl:value-of select="@name"/></caption>
61+
<tbody>
62+
<tr>
63+
<td class="table-lines">
64+
<pre>
65+
<xsl:for-each select="line">
66+
<span id="L{@number}" class="lineno"><a class="lineno" href="#L{@number}"><xsl:value-of select="@number"/></a></span><xsl:text>&#10;</xsl:text>
67+
</xsl:for-each>
68+
</pre>
69+
</td>
70+
<td class="table-code">
71+
<pre>
72+
<xsl:for-each select="line">
73+
<span class="line-{@precision}">
74+
<xsl:for-each select="char">
75+
<xsl:if test="@typestr!=''">
76+
<span class="type-char">
77+
<xsl:value-of select="@content"/>
78+
<span class="type-tooltip">
79+
<xsl:value-of select="@typestr"/>
80+
</span>
81+
</span>
82+
</xsl:if>
83+
<xsl:if test="@typestr=''">
84+
<xsl:value-of select="@content"/>
85+
</xsl:if>
86+
</xsl:for-each>
87+
</span>
88+
<xsl:text>&#10;</xsl:text>
89+
</xsl:for-each>
90+
</pre>
91+
</td>
92+
</tr>
93+
</tbody>
94+
</table>
95+
</body>
96+
</html>
97+
</xsl:template>
98+
</xsl:stylesheet>

xml/mypy-html.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,26 @@ a:hover.lineno, a:active.lineno {
102102
.line-any {
103103
background-color: #faa;
104104
}
105+
106+
.type-char {
107+
position : relative;
108+
border-bottom : 1px dotted black;
109+
}
110+
111+
.type-char:hover .type-tooltip {
112+
visibility: visible;
113+
}
114+
115+
.type-char .type-tooltip {
116+
visibility: hidden;
117+
background-color : #f80;
118+
position : absolute;
119+
z-index : 1;
120+
padding : 5px 0;
121+
text-align : center;
122+
border-radius : 6px;
123+
}
124+
125+
.type-char .type-tooltip {
126+
top : 200%;
127+
}

xml/mypy.xsd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
<xs:sequence>
3535
<xs:element name="line" minOccurs="0" maxOccurs="unbounded">
3636
<xs:complexType>
37+
<xs:sequence>
38+
<xs:element name="char" minOccurs="0" maxOccurs="unbounded">
39+
<xs:complexType>
40+
<xs:attribute name="column" type="xs:integer" use="required"/>
41+
<xs:attribute name="typestr" type="xs:string" use="required"/>
42+
<xs:attribute name="content" type="xs:string" use="required"/>
43+
</xs:complexType>
44+
</xs:element>
45+
</xs:sequence>
3746
<xs:attribute name="number" type="xs:integer" use="required"/>
3847
<xs:attribute name="precision" type="precision" use="required"/>
3948
<xs:attribute name="content" type="xs:string" use="required"/>

0 commit comments

Comments
 (0)