diff options
Diffstat (limited to 'tools/warn/html_writer.py')
-rw-r--r-- | tools/warn/html_writer.py | 673 |
1 files changed, 673 insertions, 0 deletions
diff --git a/tools/warn/html_writer.py b/tools/warn/html_writer.py new file mode 100644 index 0000000000..b8d3fe6f5d --- /dev/null +++ b/tools/warn/html_writer.py @@ -0,0 +1,673 @@ +# Lint as: python3 +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Emit warning messages to html or csv files.""" + +# To emit html page of warning messages: +# flags: --byproject, --url, --separator +# Old stuff for static html components: +# html_script_style: static html scripts and styles +# htmlbig: +# dump_stats, dump_html_prologue, dump_html_epilogue: +# emit_buttons: +# dump_fixed +# sort_warnings: +# emit_stats_by_project: +# all_patterns, +# findproject, classify_warning +# dump_html +# +# New dynamic HTML page's static JavaScript data: +# Some data are copied from Python to JavaScript, to generate HTML elements. +# FlagPlatform flags.platform +# FlagURL flags.url, used by 'android' +# FlagSeparator flags.separator, used by 'android' +# SeverityColors: list of colors for all severity levels +# SeverityHeaders: list of headers for all severity levels +# SeverityColumnHeaders: list of column_headers for all severity levels +# ProjectNames: project_names, or project_list[*][0] +# WarnPatternsSeverity: warn_patterns[*]['severity'] +# WarnPatternsDescription: warn_patterns[*]['description'] +# WarningMessages: warning_messages +# Warnings: warning_records +# StatsHeader: warning count table header row +# StatsRows: array of warning count table rows +# +# New dynamic HTML page's dynamic JavaScript data: +# +# New dynamic HTML related function to emit data: +# escape_string, strip_escape_string, emit_warning_arrays +# emit_js_data(): + +from __future__ import print_function +import cgi +import csv +import sys + +# pylint:disable=relative-beyond-top-level +# pylint:disable=g-importing-member +from .severity import Severity + + +html_head_scripts = """\ + <script type="text/javascript"> + function expand(id) { + var e = document.getElementById(id); + var f = document.getElementById(id + "_mark"); + if (e.style.display == 'block') { + e.style.display = 'none'; + f.innerHTML = '⊕'; + } + else { + e.style.display = 'block'; + f.innerHTML = '⊖'; + } + }; + function expandCollapse(show) { + for (var id = 1; ; id++) { + var e = document.getElementById(id + ""); + var f = document.getElementById(id + "_mark"); + if (!e || !f) break; + e.style.display = (show ? 'block' : 'none'); + f.innerHTML = (show ? '⊖' : '⊕'); + } + }; + </script> + <style type="text/css"> + th,td{border-collapse:collapse; border:1px solid black;} + .button{color:blue;font-size:110%;font-weight:bolder;} + .bt{color:black;background-color:transparent;border:none;outline:none; + font-size:140%;font-weight:bolder;} + .c0{background-color:#e0e0e0;} + .c1{background-color:#d0d0d0;} + .t1{border-collapse:collapse; width:100%; border:1px solid black;} + </style> + <script src="https://www.gstatic.com/charts/loader.js"></script> +""" + + +def make_writer(output_stream): + + def writer(text): + return output_stream.write(text + '\n') + + return writer + + +def html_big(param): + return '<font size="+2">' + param + '</font>' + + +def dump_html_prologue(title, writer, warn_patterns, project_names): + writer('<html>\n<head>') + writer('<title>' + title + '</title>') + writer(html_head_scripts) + emit_stats_by_project(writer, warn_patterns, project_names) + writer('</head>\n<body>') + writer(html_big(title)) + writer('<p>') + + +def dump_html_epilogue(writer): + writer('</body>\n</head>\n</html>') + + +def sort_warnings(warn_patterns): + for i in warn_patterns: + i['members'] = sorted(set(i['members'])) + + +def create_warnings(warn_patterns, project_names): + """Creates warnings s.t. + + warnings[p][s] is as specified in above docs. + + Args: + warn_patterns: list of warning patterns for specified platform + project_names: list of project names + + Returns: + 2D warnings array where warnings[p][s] is # of warnings in project name p of + severity level s + """ + # pylint:disable=g-complex-comprehension + warnings = {p: {s.value: 0 for s in Severity.levels} for p in project_names} + for i in warn_patterns: + s = i['severity'].value + for p in i['projects']: + warnings[p][s] += i['projects'][p] + return warnings + + +def get_total_by_project(warnings, project_names): + """Returns dict, project as key and # warnings for that project as value.""" + # pylint:disable=g-complex-comprehension + return { + p: sum(warnings[p][s.value] for s in Severity.levels) + for p in project_names + } + + +def get_total_by_severity(warnings, project_names): + """Returns dict, severity as key and # warnings of that severity as value.""" + # pylint:disable=g-complex-comprehension + return { + s.value: sum(warnings[p][s.value] for p in project_names) + for s in Severity.levels + } + + +def emit_table_header(total_by_severity): + """Returns list of HTML-formatted content for severity stats.""" + + stats_header = ['Project'] + for s in Severity.levels: + if total_by_severity[s.value]: + stats_header.append( + '<span style=\'background-color:{}\'>{}</span>'.format( + s.color, s.column_header)) + stats_header.append('TOTAL') + return stats_header + + +def emit_row_counts_per_project(warnings, total_by_project, total_by_severity, + project_names): + """Returns total project warnings and row of stats for each project. + + Args: + warnings: output of create_warnings(warn_patterns, project_names) + total_by_project: output of get_total_by_project(project_names) + total_by_severity: output of get_total_by_severity(project_names) + project_names: list of project names + + Returns: + total_all_projects, the total number of warnings over all projects + stats_rows, a 2d list where each row is [Project Name, <severity counts>, + total # warnings for this project] + """ + + total_all_projects = 0 + stats_rows = [] + for p in project_names: + if total_by_project[p]: + one_row = [p] + for s in Severity.levels: + if total_by_severity[s.value]: + one_row.append(warnings[p][s.value]) + one_row.append(total_by_project[p]) + stats_rows.append(one_row) + total_all_projects += total_by_project[p] + return total_all_projects, stats_rows + + +def emit_row_counts_per_severity(total_by_severity, stats_header, stats_rows, + total_all_projects, writer): + """Emits stats_header and stats_rows as specified above. + + Args: + total_by_severity: output of get_total_by_severity() + stats_header: output of emit_table_header() + stats_rows: output of emit_row_counts_per_project() + total_all_projects: output of emit_row_counts_per_project() + writer: writer returned by make_writer(output_stream) + """ + + total_all_severities = 0 + one_row = ['<b>TOTAL</b>'] + for s in Severity.levels: + if total_by_severity[s.value]: + one_row.append(total_by_severity[s.value]) + total_all_severities += total_by_severity[s.value] + one_row.append(total_all_projects) + stats_rows.append(one_row) + writer('<script>') + emit_const_string_array('StatsHeader', stats_header, writer) + emit_const_object_array('StatsRows', stats_rows, writer) + writer(draw_table_javascript) + writer('</script>') + + +def emit_stats_by_project(writer, warn_patterns, project_names): + """Dump a google chart table of warnings per project and severity.""" + + warnings = create_warnings(warn_patterns, project_names) + total_by_project = get_total_by_project(warnings, project_names) + total_by_severity = get_total_by_severity(warnings, project_names) + stats_header = emit_table_header(total_by_severity) + total_all_projects, stats_rows = \ + emit_row_counts_per_project(warnings, total_by_project, total_by_severity, project_names) + emit_row_counts_per_severity(total_by_severity, stats_header, stats_rows, + total_all_projects, writer) + + +def dump_stats(writer, warn_patterns): + """Dump some stats about total number of warnings and such.""" + + known = 0 + skipped = 0 + unknown = 0 + sort_warnings(warn_patterns) + for i in warn_patterns: + if i['severity'] == Severity.UNMATCHED: + unknown += len(i['members']) + elif i['severity'] == Severity.SKIP: + skipped += len(i['members']) + else: + known += len(i['members']) + writer('Number of classified warnings: <b>' + str(known) + '</b><br>') + writer('Number of skipped warnings: <b>' + str(skipped) + '</b><br>') + writer('Number of unclassified warnings: <b>' + str(unknown) + '</b><br>') + total = unknown + known + skipped + extra_msg = '' + if total < 1000: + extra_msg = ' (low count may indicate incremental build)' + writer('Total number of warnings: <b>' + str(total) + '</b>' + extra_msg) + + +# New base table of warnings, [severity, warn_id, project, warning_message] +# Need buttons to show warnings in different grouping options. +# (1) Current, group by severity, id for each warning pattern +# sort by severity, warn_id, warning_message +# (2) Current --byproject, group by severity, +# id for each warning pattern + project name +# sort by severity, warn_id, project, warning_message +# (3) New, group by project + severity, +# id for each warning pattern +# sort by project, severity, warn_id, warning_message +def emit_buttons(writer): + writer('<button class="button" onclick="expandCollapse(1);">' + 'Expand all warnings</button>\n' + '<button class="button" onclick="expandCollapse(0);">' + 'Collapse all warnings</button>\n' + '<button class="button" onclick="groupBySeverity();">' + 'Group warnings by severity</button>\n' + '<button class="button" onclick="groupByProject();">' + 'Group warnings by project</button><br>') + + +def all_patterns(category): + patterns = '' + for i in category['patterns']: + patterns += i + patterns += ' / ' + return patterns + + +def dump_fixed(writer, warn_patterns): + """Show which warnings no longer occur.""" + anchor = 'fixed_warnings' + mark = anchor + '_mark' + writer('\n<br><p style="background-color:lightblue"><b>' + '<button id="' + mark + '" ' + 'class="bt" onclick="expand(\'' + anchor + '\');">' + '⊕</button> Fixed warnings. ' + 'No more occurrences. Please consider turning these into ' + 'errors if possible, before they are reintroduced in to the build' + ':</b></p>') + writer('<blockquote>') + fixed_patterns = [] + for i in warn_patterns: + if not i['members']: + fixed_patterns.append(i['description'] + ' (' + all_patterns(i) + ')') + fixed_patterns = sorted(fixed_patterns) + writer('<div id="' + anchor + '" style="display:none;"><table>') + cur_row_class = 0 + for text in fixed_patterns: + cur_row_class = 1 - cur_row_class + # remove last '\n' + t = text[:-1] if text[-1] == '\n' else text + writer('<tr><td class="c' + str(cur_row_class) + '">' + t + '</td></tr>') + writer('</table></div>') + writer('</blockquote>') + + +def write_severity(csvwriter, sev, kind, warn_patterns): + """Count warnings of given severity and write CSV entries to writer.""" + total = 0 + for pattern in warn_patterns: + if pattern['severity'] == sev and pattern['members']: + n = len(pattern['members']) + total += n + warning = kind + ': ' + (pattern['description'] or '?') + csvwriter.writerow([n, '', warning]) + # print number of warnings for each project, ordered by project name + projects = sorted(pattern['projects'].keys()) + for project in projects: + csvwriter.writerow([pattern['projects'][project], project, warning]) + csvwriter.writerow([total, '', kind + ' warnings']) + return total + + +def dump_csv(csvwriter, warn_patterns): + """Dump number of warnings in CSV format to writer.""" + sort_warnings(warn_patterns) + total = 0 + for s in Severity.levels: + total += write_severity(csvwriter, s, s.column_header, warn_patterns) + csvwriter.writerow([total, '', 'All warnings']) + + +# Return s with escaped backslash and quotation characters. +def escape_string(s): + return s.replace('\\', '\\\\').replace('"', '\\"') + + +# Return s without trailing '\n' and escape the quotation characters. +def strip_escape_string(s): + if not s: + return s + s = s[:-1] if s[-1] == '\n' else s + return escape_string(s) + + +def emit_warning_array(name, writer, warn_patterns): + writer('var warning_{} = ['.format(name)) + for w in warn_patterns: + if name == 'severity': + writer('{},'.format(w[name].value)) + else: + writer('{},'.format(w[name])) + writer('];') + + +def emit_warning_arrays(writer, warn_patterns): + emit_warning_array('severity', writer, warn_patterns) + writer('var warning_description = [') + for w in warn_patterns: + if w['members']: + writer('"{}",'.format(escape_string(w['description']))) + else: + writer('"",') # no such warning + writer('];') + + +scripts_for_warning_groups = """ + function compareMessages(x1, x2) { // of the same warning type + return (WarningMessages[x1[2]] <= WarningMessages[x2[2]]) ? -1 : 1; + } + function byMessageCount(x1, x2) { + return x2[2] - x1[2]; // reversed order + } + function bySeverityMessageCount(x1, x2) { + // orer by severity first + if (x1[1] != x2[1]) + return x1[1] - x2[1]; + return byMessageCount(x1, x2); + } + const ParseLinePattern = /^([^ :]+):(\\d+):(.+)/; + function addURL(line) { // used by Android + if (FlagURL == "") return line; + if (FlagSeparator == "") { + return line.replace(ParseLinePattern, + "<a target='_blank' href='" + FlagURL + "/$1'>$1</a>:$2:$3"); + } + return line.replace(ParseLinePattern, + "<a target='_blank' href='" + FlagURL + "/$1" + FlagSeparator + + "$2'>$1:$2</a>:$3"); + } + function addURLToLine(line, link) { // used by Chrome + let line_split = line.split(":"); + let path = line_split.slice(0,3).join(":"); + let msg = line_split.slice(3).join(":"); + let html_link = `<a target="_blank" href="${link}">${path}</a>${msg}`; + return html_link; + } + function createArrayOfDictionaries(n) { + var result = []; + for (var i=0; i<n; i++) result.push({}); + return result; + } + function groupWarningsBySeverity() { + // groups is an array of dictionaries, + // each dictionary maps from warning type to array of warning messages. + var groups = createArrayOfDictionaries(SeverityColors.length); + for (var i=0; i<Warnings.length; i++) { + var w = Warnings[i][0]; + var s = WarnPatternsSeverity[w]; + var k = w.toString(); + if (!(k in groups[s])) + groups[s][k] = []; + groups[s][k].push(Warnings[i]); + } + return groups; + } + function groupWarningsByProject() { + var groups = createArrayOfDictionaries(ProjectNames.length); + for (var i=0; i<Warnings.length; i++) { + var w = Warnings[i][0]; + var p = Warnings[i][1]; + var k = w.toString(); + if (!(k in groups[p])) + groups[p][k] = []; + groups[p][k].push(Warnings[i]); + } + return groups; + } + var GlobalAnchor = 0; + function createWarningSection(header, color, group) { + var result = ""; + var groupKeys = []; + var totalMessages = 0; + for (var k in group) { + totalMessages += group[k].length; + groupKeys.push([k, WarnPatternsSeverity[parseInt(k)], group[k].length]); + } + groupKeys.sort(bySeverityMessageCount); + for (var idx=0; idx<groupKeys.length; idx++) { + var k = groupKeys[idx][0]; + var messages = group[k]; + var w = parseInt(k); + var wcolor = SeverityColors[WarnPatternsSeverity[w]]; + var description = WarnPatternsDescription[w]; + if (description.length == 0) + description = "???"; + GlobalAnchor += 1; + result += "<table class='t1'><tr bgcolor='" + wcolor + "'><td>" + + "<button class='bt' id='" + GlobalAnchor + "_mark" + + "' onclick='expand(\\"" + GlobalAnchor + "\\");'>" + + "⊕</button> " + + description + " (" + messages.length + ")</td></tr></table>"; + result += "<div id='" + GlobalAnchor + + "' style='display:none;'><table class='t1'>"; + var c = 0; + messages.sort(compareMessages); + if (FlagPlatform == "chrome") { + for (var i=0; i<messages.length; i++) { + result += "<tr><td class='c" + c + "'>" + + addURLToLine(WarningMessages[messages[i][2]], WarningLinks[messages[i][3]]) + "</td></tr>"; + c = 1 - c; + } + } else { + for (var i=0; i<messages.length; i++) { + result += "<tr><td class='c" + c + "'>" + + addURL(WarningMessages[messages[i][2]]) + "</td></tr>"; + c = 1 - c; + } + } + result += "</table></div>"; + } + if (result.length > 0) { + return "<br><span style='background-color:" + color + "'><b>" + + header + ": " + totalMessages + + "</b></span><blockquote><table class='t1'>" + + result + "</table></blockquote>"; + + } + return ""; // empty section + } + function generateSectionsBySeverity() { + var result = ""; + var groups = groupWarningsBySeverity(); + for (s=0; s<SeverityColors.length; s++) { + result += createWarningSection(SeverityHeaders[s], SeverityColors[s], + groups[s]); + } + return result; + } + function generateSectionsByProject() { + var result = ""; + var groups = groupWarningsByProject(); + for (i=0; i<groups.length; i++) { + result += createWarningSection(ProjectNames[i], 'lightgrey', groups[i]); + } + return result; + } + function groupWarnings(generator) { + GlobalAnchor = 0; + var e = document.getElementById("warning_groups"); + e.innerHTML = generator(); + } + function groupBySeverity() { + groupWarnings(generateSectionsBySeverity); + } + function groupByProject() { + groupWarnings(generateSectionsByProject); + } +""" + + +# Emit a JavaScript const string +def emit_const_string(name, value, writer): + writer('const ' + name + ' = "' + escape_string(value) + '";') + + +# Emit a JavaScript const integer array. +def emit_const_int_array(name, array, writer): + writer('const ' + name + ' = [') + for n in array: + writer(str(n) + ',') + writer('];') + + +# Emit a JavaScript const string array. +def emit_const_string_array(name, array, writer): + writer('const ' + name + ' = [') + for s in array: + writer('"' + strip_escape_string(s) + '",') + writer('];') + + +# Emit a JavaScript const string array for HTML. +def emit_const_html_string_array(name, array, writer): + writer('const ' + name + ' = [') + for s in array: + # Not using html.escape yet, to work for both python 2 and 3, + # until all users switch to python 3. + # pylint:disable=deprecated-method + writer('"' + cgi.escape(strip_escape_string(s)) + '",') + writer('];') + + +# Emit a JavaScript const object array. +def emit_const_object_array(name, array, writer): + writer('const ' + name + ' = [') + for x in array: + writer(str(x) + ',') + writer('];') + + +def emit_js_data(writer, flags, warning_messages, warning_links, + warning_records, warn_patterns, project_names): + """Dump dynamic HTML page's static JavaScript data.""" + emit_const_string('FlagPlatform', flags.platform, writer) + emit_const_string('FlagURL', flags.url, writer) + emit_const_string('FlagSeparator', flags.separator, writer) + emit_const_string_array('SeverityColors', [s.color for s in Severity.levels], + writer) + emit_const_string_array('SeverityHeaders', + [s.header for s in Severity.levels], writer) + emit_const_string_array('SeverityColumnHeaders', + [s.column_header for s in Severity.levels], writer) + emit_const_string_array('ProjectNames', project_names, writer) + # pytype: disable=attribute-error + emit_const_int_array('WarnPatternsSeverity', + [w['severity'].value for w in warn_patterns], writer) + # pytype: enable=attribute-error + emit_const_html_string_array('WarnPatternsDescription', + [w['description'] for w in warn_patterns], + writer) + emit_const_html_string_array('WarningMessages', warning_messages, writer) + emit_const_object_array('Warnings', warning_records, writer) + if flags.platform == 'chrome': + emit_const_html_string_array('WarningLinks', warning_links, writer) + + +draw_table_javascript = """ +google.charts.load('current', {'packages':['table']}); +google.charts.setOnLoadCallback(drawTable); +function drawTable() { + var data = new google.visualization.DataTable(); + data.addColumn('string', StatsHeader[0]); + for (var i=1; i<StatsHeader.length; i++) { + data.addColumn('number', StatsHeader[i]); + } + data.addRows(StatsRows); + for (var i=0; i<StatsRows.length; i++) { + for (var j=0; j<StatsHeader.length; j++) { + data.setProperty(i, j, 'style', 'border:1px solid black;'); + } + } + var table = new google.visualization.Table( + document.getElementById('stats_table')); + table.draw(data, {allowHtml: true, alternatingRowStyle: true}); +} +""" + + +def dump_html(flags, output_stream, warning_messages, warning_links, + warning_records, header_str, warn_patterns, project_names): + """Dump the flags output to output_stream.""" + writer = make_writer(output_stream) + dump_html_prologue('Warnings for ' + header_str, writer, warn_patterns, + project_names) + dump_stats(writer, warn_patterns) + writer('<br><div id="stats_table"></div><br>') + writer('\n<script>') + emit_js_data(writer, flags, warning_messages, warning_links, warning_records, + warn_patterns, project_names) + writer(scripts_for_warning_groups) + writer('</script>') + emit_buttons(writer) + # Warning messages are grouped by severities or project names. + writer('<br><div id="warning_groups"></div>') + if flags.byproject: + writer('<script>groupByProject();</script>') + else: + writer('<script>groupBySeverity();</script>') + dump_fixed(writer, warn_patterns) + dump_html_epilogue(writer) + + +def write_html(flags, project_names, warn_patterns, html_path, warning_messages, + warning_links, warning_records, header_str): + """Write warnings html file.""" + if html_path: + with open(html_path, 'w') as f: + dump_html(flags, f, warning_messages, warning_links, warning_records, + header_str, warn_patterns, project_names) + + +def write_out_csv(flags, warn_patterns, warning_messages, warning_links, + warning_records, header_str, project_names): + """Write warnings csv file.""" + if flags.csvpath: + with open(flags.csvpath, 'w') as f: + dump_csv(csv.writer(f, lineterminator='\n'), warn_patterns) + + if flags.gencsv: + dump_csv(csv.writer(sys.stdout, lineterminator='\n'), warn_patterns) + else: + dump_html(flags, sys.stdout, warning_messages, warning_links, + warning_records, header_str, warn_patterns, project_names) |