[lint/dvsim] This adds a lint flow to dvsim

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/util/dvsim/LintCfg.py b/util/dvsim/LintCfg.py
new file mode 100644
index 0000000..3209bf7
--- /dev/null
+++ b/util/dvsim/LintCfg.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""
+Class describing lint configuration object
+"""
+
+import logging as log
+import sys
+from pathlib import Path
+
+from tabulate import tabulate
+
+from .Deploy import *
+from .Modes import *
+from .OneShotCfg import OneShotCfg
+from .utils import *
+
+
+# helper function for printing messages
+def _print_msg_list(msg_list_name, msg_list):
+    md_results = ""
+    if msg_list:
+        md_results += "### %s\n" % msg_list_name
+        md_results += "```\n"
+        for msg in msg_list:
+            msg_parts = msg.split()
+            md_results += msg + "\n\n"
+        md_results += "```\n"
+    return md_results
+
+
+class LintCfg(OneShotCfg):
+    """Derivative class for linting purposes.
+    """
+    def __init__(self, flow_cfg_file, proj_root, args):
+        super().__init__(flow_cfg_file, proj_root, args)
+
+    def __post_init__(self):
+        super().__post_init__()
+        # Set the title for lint results.
+        self.results_title = self.name.upper() + " Lint Results"
+
+
+    @staticmethod
+    def create_instance(flow_cfg_file, proj_root, args):
+        '''Create a new instance of this class as with given parameters.
+        '''
+        return LintCfg(flow_cfg_file, proj_root, args)
+
+    def gen_results_summary(self):
+        '''
+        Gathers the aggregated results from all sub configs
+        '''
+
+        # Generate results table for runs. Note that we build a simple md and
+        # a marked up md version here in parallel.
+        log.info("Create summary of lint results")
+
+        results_str = "## " + self.results_title + " (Summary)\n\n"
+        results_str += "### " + self.timestamp_long + "\n\n"
+
+        header = [
+            "Name", "Tool Warnings", "Tool Errors", "Lint Warnings",
+            "Lint Errors"
+        ]
+        colalign = ("center", ) * len(header)
+        table = [header]
+
+        for cfg in self.cfgs:
+
+            results_page = cfg.results_server_dir + '/results.html'
+            results_page_url = results_page.replace(
+                cfg.results_server_prefix, cfg.results_server_url_prefix)
+            name_with_link = "[" + cfg.name.upper(
+            ) + "](" + results_page_url + ")"
+            table.append([
+                name_with_link,
+                str(len(cfg.result_summary["warnings"])) + " W",
+                str(len(cfg.result_summary["errors"])) + " E",
+                str(len(cfg.result_summary["lint_warnings"])) + " W",
+                str(len(cfg.result_summary["lint_errors"])) + " E"
+            ])
+
+        if len(table) > 1:
+            self.results_summary_md = results_str + tabulate(
+                table, headers="firstrow", tablefmt="pipe",
+                colalign=colalign) + "\n"
+        else:
+            self.results_summary_md = results_str + "\nNo results to display.\n"
+
+        print(self.results_summary_md)
+
+        # Return only the tables
+        return self.results_summary_md
+
+    def _gen_results(self):
+        # '''
+        # The function is called after the regression has completed. It looks
+        # for a regr_results.hjson file with aggregated results from the lint run.
+        # The hjson needs to have the following (potentially empty) fields
+        #
+        # {
+        #   tool: ""
+        #   errors: []
+        #   warnings: []
+        #   lint_errors: []
+        #   lint_warning: []
+        #   lint_infos: []
+        # }
+        #
+        # where each entry is a string representing a lint message. This allows
+        # to reuse the same LintCfg class with different tools since just the
+        # parsing script that transforms the tool output into the hjson above
+        # needs to be adapted.
+        #
+        # note that if this is a master config, the results will
+        # be generated using the _gen_results_summary function
+        # '''
+
+        # Generate results table for runs.
+        # Note that we build a simple md and a marked up md version
+        # here in parallel.
+        results_str = "## " + self.results_title + "\n\n"
+        results_str += "### " + self.timestamp_long + "\n"
+        results_str += "### Lint Tool: " + self.tool.upper() + "\n\n"
+
+
+        header = [
+            "Build Mode", "Tool Warnings", "Tool Errors", "Lint Warnings",
+            "Lint Errors"
+        ]
+        colalign = ("center", ) * len(header)
+        table = [header]
+
+        # aggregated counts
+        self.result_summary["warnings"] = []
+        self.result_summary["errors"] = []
+        self.result_summary["lint_warnings"] = []
+        self.result_summary["lint_errors"] = []
+
+        fail_msgs = ""
+        for mode in self.build_modes:
+
+            result_data = Path(
+                subst_wildcards(self.build_dir, {"build_mode": mode.name}) +
+                '/results.hjson')
+            log.info("looking for result data file at %s", result_data)
+
+            try:
+                with open(result_data, "r") as results_file:
+                    self.result = hjson.load(results_file, use_decimal=True)
+            except IOError as err:
+                log.warning("%s", err)
+                self.result = {
+                    "tool": "",
+                    "errors": ["IOError: %s" % err],
+                    "warnings": [],
+                    "lint_errors": [],
+                    "lint_warnings": [],
+                    "lint_infos": []
+                }
+            if self.result:
+                table.append([
+                    mode.name,
+                    str(len(self.result["warnings"])) + " W ",
+                    str(len(self.result["errors"])) + " E",
+                    # We currently do not publish these infos at
+                    # the moment len(self.result["lint_infos"]),
+                    str(len(self.result["lint_warnings"])) + " W",
+                    str(len(self.result["lint_errors"])) + " E"
+                ])
+            else:
+                self.result = {
+                    "tool": "",
+                    "errors": [],
+                    "warnings": [],
+                    "lint_errors": [],
+                    "lint_warnings": [],
+                    "lint_infos": []
+                }
+
+            self.result_summary["warnings"] += self.result["warnings"]
+            self.result_summary["errors"] += self.result["errors"]
+            self.result_summary["lint_warnings"] += self.result[
+                "lint_warnings"]
+            self.result_summary["lint_errors"] += self.result["lint_errors"]
+
+            # Append detailed messages if they exist
+            if sum([
+                    len(self.result["warnings"]),
+                    len(self.result["errors"]),
+                    len(self.result["lint_warnings"]),
+                    len(self.result["lint_errors"])
+            ]):
+                fail_msgs += "\n## Errors and Warnings for Build Mode `'" + mode.name + "'`\n"
+                fail_msgs += _print_msg_list("Tool Errors",
+                                             self.result["errors"])
+                fail_msgs += _print_msg_list("Tool Warnings",
+                                             self.result["warnings"])
+                fail_msgs += _print_msg_list("Lint Errors",
+                                             self.result["lint_errors"])
+                fail_msgs += _print_msg_list("Lint Warnings",
+                                             self.result["lint_warnings"])
+                #fail_msgs += _print_msg_list("Lint Infos", results["lint_infos"])
+
+        if len(table) > 1:
+            self.results_md = results_str + tabulate(
+                table, headers="firstrow", tablefmt="pipe",
+                colalign=colalign) + "\n" + fail_msgs
+        else:
+            self.results_md = results_str + "\nNo results to display.\n"
+
+        # Write results to the scratch area
+        self.results_file = self.scratch_path + "/results_" + self.timestamp + ".md"
+        log.info("Detailed results are available at %s", self.results_file)
+        with open(self.results_file, 'w') as f:
+            f.write(self.results_md)
+
+        return self.results_md