forked from platformio/platformio-core
* Improved file filtering for the Static Analysis feature * Better handling of legacy "check_patterns" option * Rename "check_src_filter" to plural form to better represent functionality * Move to plural forms of filter variables
252 lines
8.2 KiB
Python
252 lines
8.2 KiB
Python
# Copyright (c) 2020-present PlatformIO <contact@platformio.org>
|
|
#
|
|
# 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.
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from xml.etree.ElementTree import fromstring
|
|
|
|
import click
|
|
|
|
from platformio import proc
|
|
from platformio.check.defect import DefectItem
|
|
from platformio.check.tools.base import CheckToolBase
|
|
from platformio.compat import IS_WINDOWS
|
|
|
|
|
|
class PvsStudioCheckTool(CheckToolBase): # pylint: disable=too-many-instance-attributes
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._tmp_dir = tempfile.mkdtemp(prefix="piocheck")
|
|
self._tmp_preprocessed_file = self._generate_tmp_file_path() + ".i"
|
|
self._tmp_output_file = self._generate_tmp_file_path() + ".pvs"
|
|
self._tmp_cfg_file = self._generate_tmp_file_path() + ".cfg"
|
|
self._tmp_cmd_file = self._generate_tmp_file_path() + ".cmd"
|
|
self.tool_path = os.path.join(
|
|
self.get_tool_dir("tool-pvs-studio"),
|
|
"x64" if IS_WINDOWS else "bin",
|
|
"pvs-studio",
|
|
)
|
|
|
|
with open(self._tmp_cfg_file, mode="w", encoding="utf8") as fp:
|
|
fp.write(
|
|
"exclude-path = "
|
|
+ self.config.get("platformio", "packages_dir").replace("\\", "/")
|
|
)
|
|
|
|
with open(self._tmp_cmd_file, mode="w", encoding="utf8") as fp:
|
|
fp.write(
|
|
" ".join(
|
|
['-I"%s"' % inc.replace("\\", "/") for inc in self.cpp_includes]
|
|
)
|
|
)
|
|
|
|
def tool_output_filter(self, line): # pylint: disable=arguments-differ
|
|
if any(
|
|
err_msg in line.lower()
|
|
for err_msg in (
|
|
"license was not entered",
|
|
"license information is incorrect",
|
|
)
|
|
):
|
|
self._bad_input = True
|
|
return line
|
|
|
|
def _process_defects(self, defects):
|
|
for defect in defects:
|
|
if not isinstance(defect, DefectItem):
|
|
return
|
|
if defect.severity not in self.options["severity"]:
|
|
return
|
|
self._defects.append(defect)
|
|
if self._on_defect_callback:
|
|
self._on_defect_callback(defect)
|
|
|
|
def _demangle_report(self, output_file):
|
|
converter_tool = os.path.join(
|
|
self.get_tool_dir("tool-pvs-studio"),
|
|
"HtmlGenerator" if IS_WINDOWS else os.path.join("bin", "plog-converter"),
|
|
)
|
|
|
|
cmd = (
|
|
converter_tool,
|
|
"-t",
|
|
"xml",
|
|
output_file,
|
|
"-m",
|
|
"cwe",
|
|
"-m",
|
|
"misra",
|
|
"-a",
|
|
# Enable all possible analyzers and defect levels
|
|
"GA:1,2,3;64:1,2,3;OP:1,2,3;CS:1,2,3;MISRA:1,2,3",
|
|
"--cerr",
|
|
)
|
|
|
|
result = proc.exec_command(cmd)
|
|
if result["returncode"] != 0:
|
|
click.echo(result["err"])
|
|
self._bad_input = True
|
|
|
|
return result["err"]
|
|
|
|
def parse_defects(self, output_file):
|
|
defects = []
|
|
|
|
report = self._demangle_report(output_file)
|
|
if not report:
|
|
self._bad_input = True
|
|
return []
|
|
|
|
try:
|
|
defects_data = fromstring(report)
|
|
except: # pylint: disable=bare-except
|
|
click.echo("Error: Couldn't decode generated report!")
|
|
self._bad_input = True
|
|
return []
|
|
|
|
for table in defects_data.iter("PVS-Studio_Analysis_Log"):
|
|
message = table.find("Message").text
|
|
category = table.find("ErrorType").text
|
|
line = table.find("Line").text
|
|
file_ = table.find("File").text
|
|
defect_id = table.find("ErrorCode").text
|
|
cwe = table.find("CWECode")
|
|
cwe_id = None
|
|
if cwe is not None:
|
|
cwe_id = cwe.text.lower().replace("cwe-", "")
|
|
misra = table.find("MISRA")
|
|
if misra is not None:
|
|
message += " [%s]" % misra.text
|
|
|
|
severity = DefectItem.SEVERITY_LOW
|
|
if category == "error":
|
|
severity = DefectItem.SEVERITY_HIGH
|
|
elif category == "warning":
|
|
severity = DefectItem.SEVERITY_MEDIUM
|
|
|
|
defects.append(
|
|
DefectItem(
|
|
severity, category, message, file_, line, id=defect_id, cwe=cwe_id
|
|
)
|
|
)
|
|
|
|
return defects
|
|
|
|
def configure_command(self, src_file): # pylint: disable=arguments-differ
|
|
if os.path.isfile(self._tmp_output_file):
|
|
os.remove(self._tmp_output_file)
|
|
|
|
if not os.path.isfile(self._tmp_preprocessed_file):
|
|
click.echo("Error: Missing preprocessed file for '%s'" % src_file)
|
|
return ""
|
|
|
|
cmd = [
|
|
self.tool_path,
|
|
"--skip-cl-exe",
|
|
"yes",
|
|
"--language",
|
|
"C" if src_file.endswith(".c") else "C++",
|
|
"--preprocessor",
|
|
"gcc",
|
|
"--cfg",
|
|
self._tmp_cfg_file,
|
|
"--source-file",
|
|
src_file,
|
|
"--i-file",
|
|
self._tmp_preprocessed_file,
|
|
"--output-file",
|
|
self._tmp_output_file,
|
|
]
|
|
|
|
flags = self.get_flags("pvs-studio")
|
|
if not self.is_flag_set("--platform", flags):
|
|
cmd.append("--platform=arm")
|
|
cmd.extend(flags)
|
|
|
|
return cmd
|
|
|
|
def _generate_tmp_file_path(self):
|
|
# pylint: disable=protected-access
|
|
return os.path.join(self._tmp_dir, next(tempfile._get_candidate_names()))
|
|
|
|
def _prepare_preprocessed_file(self, src_file):
|
|
if os.path.isfile(self._tmp_preprocessed_file):
|
|
os.remove(self._tmp_preprocessed_file)
|
|
|
|
flags = self.cxx_flags
|
|
compiler = self.cxx_path
|
|
if src_file.endswith(".c"):
|
|
flags = self.cc_flags
|
|
compiler = self.cc_path
|
|
|
|
cmd = [
|
|
compiler,
|
|
'"%s"' % src_file,
|
|
"-E",
|
|
"-o",
|
|
'"%s"' % self._tmp_preprocessed_file,
|
|
]
|
|
cmd.extend([f for f in flags if f])
|
|
cmd.extend(['"-D%s"' % d.replace('"', '\\"') for d in self.cpp_defines])
|
|
cmd.append('@"%s"' % self._tmp_cmd_file)
|
|
|
|
# Explicitly specify C++ as the language used in .ino files
|
|
if src_file.endswith(".ino"):
|
|
cmd.insert(1, "-xc++")
|
|
|
|
result = proc.exec_command(" ".join(cmd), shell=True)
|
|
if result["returncode"] != 0 or result["err"]:
|
|
if self.options.get("verbose"):
|
|
click.echo(" ".join(cmd))
|
|
click.echo(result["err"])
|
|
self._bad_input = True
|
|
|
|
def clean_up(self):
|
|
super().clean_up()
|
|
if os.path.isdir(self._tmp_dir):
|
|
shutil.rmtree(self._tmp_dir)
|
|
|
|
@staticmethod
|
|
def is_check_successful(cmd_result):
|
|
return (
|
|
"license" not in cmd_result["err"].lower() and cmd_result["returncode"] == 0
|
|
)
|
|
|
|
def check(self, on_defect_callback=None):
|
|
self._on_defect_callback = on_defect_callback
|
|
for scope, files in self.get_project_target_files(
|
|
self.project_dir, self.options["src_filters"]
|
|
).items():
|
|
if scope not in ("c", "c++"):
|
|
continue
|
|
for src_file in files:
|
|
self._prepare_preprocessed_file(src_file)
|
|
cmd = self.configure_command(src_file)
|
|
if self.options.get("verbose"):
|
|
click.echo(" ".join(cmd))
|
|
if not cmd:
|
|
self._bad_input = True
|
|
continue
|
|
|
|
result = self.execute_check_cmd(cmd)
|
|
if result["returncode"] != 0:
|
|
continue
|
|
|
|
self._process_defects(self.parse_defects(self._tmp_output_file))
|
|
|
|
self.clean_up()
|
|
|
|
return self._bad_input
|