1+ """Classes for producing HTML reports about type checking results."""
2+
3+ from __future__ import annotations
4+
5+ import collections
6+ import os
7+ import shutil
8+ from typing import Any
9+
10+ from mypy import stats
11+ from mypy .nodes import Expression , MypyFile
12+ from mypy .options import Options
13+ from mypy .report import AbstractReporter , FileInfo , iterate_python_lines , register_reporter , should_skip_path
14+ from mypy .types import Type , TypeOfAny
15+ from mypy .version import __version__
16+
17+ # Map of TypeOfAny enum values to descriptive strings
18+ type_of_any_name_map = {
19+ TypeOfAny .unannotated : "Unannotated" ,
20+ TypeOfAny .explicit : "Explicit" ,
21+ TypeOfAny .from_unimported_type : "Unimported" ,
22+ TypeOfAny .from_omitted_generics : "Omitted Generics" ,
23+ TypeOfAny .from_error : "Error" ,
24+ TypeOfAny .special_form : "Special Form" ,
25+ TypeOfAny .implementation_artifact : "Implementation Artifact" ,
26+ }
27+
28+
29+ class MemoryHtmlReporter (AbstractReporter ):
30+ """Internal reporter that generates HTML in memory.
31+
32+ This is used by the HTML reporter to avoid duplication.
33+ """
34+
35+ def __init__ (self , reports : Any , output_dir : str ) -> None :
36+ super ().__init__ (reports , output_dir )
37+ self .css_html_path = os .path .join (reports .data_dir , "xml" , "mypy-html.css" )
38+ self .last_html : dict [str , str ] = {} # Maps file paths to HTML content
39+ self .index_html : str | None = None
40+ self .files : list [FileInfo ] = []
41+
42+ def on_file (
43+ self ,
44+ tree : MypyFile ,
45+ modules : dict [str , MypyFile ],
46+ type_map : dict [Expression , Type ],
47+ options : Options ,
48+ ) -> None :
49+ try :
50+ path = os .path .relpath (tree .path )
51+ except ValueError :
52+ return
53+
54+ if should_skip_path (path ) or os .path .isdir (path ):
55+ return # `path` can sometimes be a directory, see #11334
56+
57+ visitor = stats .StatisticsVisitor (
58+ inferred = True ,
59+ filename = tree .fullname ,
60+ modules = modules ,
61+ typemap = type_map ,
62+ all_nodes = True ,
63+ )
64+ tree .accept (visitor )
65+
66+ file_info = FileInfo (path , tree ._fullname )
67+
68+ # Generate HTML for this file
69+ html_lines = [
70+ "<!DOCTYPE html>" ,
71+ "<html>" ,
72+ "<head>" ,
73+ " <meta charset='utf-8'>" ,
74+ " <title>Mypy Report: " + path + "</title>" ,
75+ " <link rel='stylesheet' href='../mypy-html.css'>" ,
76+ " <style>" ,
77+ " body { font-family: Arial, sans-serif; margin: 20px; }" ,
78+ " h1 { color: #333; }" ,
79+ " table { border-collapse: collapse; width: 100%; }" ,
80+ " th { background-color: #f2f2f2; text-align: left; padding: 8px; }" ,
81+ " td { padding: 8px; border-bottom: 1px solid #ddd; }" ,
82+ " tr.precise { background-color: #dff0d8; }" ,
83+ " tr.imprecise { background-color: #fcf8e3; }" ,
84+ " tr.any { background-color: #f2dede; }" ,
85+ " tr.empty, tr.unanalyzed { background-color: #f9f9f9; }" ,
86+ " pre { margin: 0; white-space: pre-wrap; }" ,
87+ " </style>" ,
88+ "</head>" ,
89+ "<body>" ,
90+ f" <h1>Mypy Type Check Report for { path } </h1>" ,
91+ " <table>" ,
92+ " <tr>" ,
93+ " <th>Line</th>" ,
94+ " <th>Precision</th>" ,
95+ " <th>Code</th>" ,
96+ " <th>Notes</th>" ,
97+ " </tr>"
98+ ]
99+
100+ for lineno , line_text in iterate_python_lines (path ):
101+ status = visitor .line_map .get (lineno , stats .TYPE_EMPTY )
102+ file_info .counts [status ] += 1
103+
104+ precision = stats .precision_names [status ]
105+ any_info = self ._get_any_info_for_line (visitor , lineno )
106+
107+ # Escape HTML special characters in the line content
108+ content = line_text .rstrip ("\n " )
109+ content = content .replace ("&" , "&" ).replace ("<" , "<" ).replace (">" , ">" )
110+
111+ # Add CSS class based on precision
112+ css_class = precision .lower ()
113+
114+ html_lines .append (
115+ f" <tr class='{ css_class } '>"
116+ f"<td>{ lineno } </td>"
117+ f"<td>{ precision } </td>"
118+ f"<td><pre>{ content } </pre></td>"
119+ f"<td>{ any_info } </td>"
120+ "</tr>"
121+ )
122+
123+ html_lines .extend ([
124+ " </table>" ,
125+ "</body>" ,
126+ "</html>"
127+ ])
128+
129+ self .last_html [path ] = "\n " .join (html_lines )
130+ self .files .append (file_info )
131+
132+ @staticmethod
133+ def _get_any_info_for_line (visitor : stats .StatisticsVisitor , lineno : int ) -> str :
134+ if lineno in visitor .any_line_map :
135+ result = "Any Types on this line: "
136+ counter : collections .Counter [int ] = collections .Counter ()
137+ for typ in visitor .any_line_map [lineno ]:
138+ counter [typ .type_of_any ] += 1
139+ for any_type , occurrences in counter .items ():
140+ result += f"<br>{ type_of_any_name_map [any_type ]} (x{ occurrences } )"
141+ return result
142+ else :
143+ return ""
144+
145+ def on_finish (self ) -> None :
146+ output_files = sorted (self .files , key = lambda x : x .module )
147+
148+ # Generate index HTML
149+ html_lines = [
150+ "<!DOCTYPE html>" ,
151+ "<html>" ,
152+ "<head>" ,
153+ " <meta charset='utf-8'>" ,
154+ " <title>Mypy Report Index</title>" ,
155+ " <link rel='stylesheet' href='mypy-html.css'>" ,
156+ " <style>" ,
157+ " body { font-family: Arial, sans-serif; margin: 20px; }" ,
158+ " h1 { color: #333; }" ,
159+ " table { border-collapse: collapse; width: 100%; }" ,
160+ " th { background-color: #f2f2f2; text-align: left; padding: 8px; }" ,
161+ " td { padding: 8px; border-bottom: 1px solid #ddd; }" ,
162+ " a { color: #337ab7; text-decoration: none; }" ,
163+ " a:hover { text-decoration: underline; }" ,
164+ " </style>" ,
165+ "</head>" ,
166+ "<body>" ,
167+ " <h1>Mypy Type Check Report</h1>" ,
168+ " <p>Generated with mypy " + __version__ + "</p>" ,
169+ " <table>" ,
170+ " <tr>" ,
171+ " <th>Module</th>" ,
172+ " <th>File</th>" ,
173+ " <th>Precise</th>" ,
174+ " <th>Imprecise</th>" ,
175+ " <th>Any</th>" ,
176+ " <th>Empty</th>" ,
177+ " <th>Unanalyzed</th>" ,
178+ " <th>Total</th>" ,
179+ " </tr>"
180+ ]
181+
182+ for file_info in output_files :
183+ counts = file_info .counts
184+ html_lines .append (
185+ f" <tr>"
186+ f"<td>{ file_info .module } </td>"
187+ f"<td><a href='html/{ file_info .name } .html'>{ file_info .name } </a></td>"
188+ f"<td>{ counts [stats .TYPE_PRECISE ]} </td>"
189+ f"<td>{ counts [stats .TYPE_IMPRECISE ]} </td>"
190+ f"<td>{ counts [stats .TYPE_ANY ]} </td>"
191+ f"<td>{ counts [stats .TYPE_EMPTY ]} </td>"
192+ f"<td>{ counts [stats .TYPE_UNANALYZED ]} </td>"
193+ f"<td>{ file_info .total ()} </td>"
194+ "</tr>"
195+ )
196+
197+ html_lines .extend ([
198+ " </table>" ,
199+ "</body>" ,
200+ "</html>"
201+ ])
202+
203+ self .index_html = "\n " .join (html_lines )
204+
205+
206+ class HtmlReporter (AbstractReporter ):
207+ """Public reporter that exports HTML directly.
208+
209+ This reporter generates HTML files for each Python module and an index.html file.
210+ """
211+
212+ def __init__ (self , reports : Any , output_dir : str ) -> None :
213+ super ().__init__ (reports , output_dir )
214+
215+ memory_reporter = reports .add_report ("memory-html" , "<memory>" )
216+ assert isinstance (memory_reporter , MemoryHtmlReporter )
217+ # The dependency will be called first.
218+ self .memory_html = memory_reporter
219+
220+ def on_file (
221+ self ,
222+ tree : MypyFile ,
223+ modules : dict [str , MypyFile ],
224+ type_map : dict [Expression , Type ],
225+ options : Options ,
226+ ) -> None :
227+ last_html = self .memory_html .last_html
228+ if not last_html :
229+ return
230+
231+ path = os .path .relpath (tree .path )
232+ if path .startswith (".." ) or path not in last_html :
233+ return
234+
235+ out_path = os .path .join (self .output_dir , "html" , path + ".html" )
236+ os .makedirs (os .path .dirname (out_path ), exist_ok = True )
237+
238+ with open (out_path , "w" , encoding = "utf-8" ) as out_file :
239+ out_file .write (last_html [path ])
240+
241+ def on_finish (self ) -> None :
242+ index_html = self .memory_html .index_html
243+ if index_html is None :
244+ return
245+
246+ out_path = os .path .join (self .output_dir , "index.html" )
247+ out_css = os .path .join (self .output_dir , "mypy-html.css" )
248+
249+ with open (out_path , "w" , encoding = "utf-8" ) as out_file :
250+ out_file .write (index_html )
251+
252+ # Copy CSS file if it exists
253+ if os .path .exists (self .memory_html .css_html_path ):
254+ shutil .copyfile (self .memory_html .css_html_path , out_css )
255+ else :
256+ # Create a basic CSS file if the original doesn't exist
257+ with open (out_css , "w" , encoding = "utf-8" ) as css_file :
258+ css_file .write ("""
259+ body { font-family: Arial, sans-serif; margin: 20px; }
260+ h1 { color: #333; }
261+ table { border-collapse: collapse; width: 100%; }
262+ th { background-color: #f2f2f2; text-align: left; padding: 8px; }
263+ td { padding: 8px; border-bottom: 1px solid #ddd; }
264+ tr.precise { background-color: #dff0d8; }
265+ tr.imprecise { background-color: #fcf8e3; }
266+ tr.any { background-color: #f2dede; }
267+ tr.empty, tr.unanalyzed { background-color: #f9f9f9; }
268+ pre { margin: 0; white-space: pre-wrap; }
269+ a { color: #337ab7; text-decoration: none; }
270+ a:hover { text-decoration: underline; }
271+ """ )
272+
273+ print ("Generated HTML report:" , os .path .abspath (out_path ))
274+
275+
276+ # Register the reporters
277+ register_reporter ("memory-html" , MemoryHtmlReporter )
278+ register_reporter ("html-direct" , HtmlReporter )
0 commit comments