diff --git a/platformio/check/tools/__init__.py b/platformio/check/tools/__init__.py index 58a9263d2d..425d701238 100644 --- a/platformio/check/tools/__init__.py +++ b/platformio/check/tools/__init__.py @@ -15,7 +15,6 @@ from platformio import exception from platformio.check.tools.clangtidy import ClangtidyCheckTool from platformio.check.tools.cppcheck import CppcheckCheckTool -from platformio.check.tools.pvsstudio import PvsStudioCheckTool class CheckToolFactory: @@ -26,8 +25,6 @@ def new(tool, project_dir, config, envname, options): cls = CppcheckCheckTool elif tool == "clangtidy": cls = ClangtidyCheckTool - elif tool == "pvs-studio": - cls = PvsStudioCheckTool else: raise exception.PlatformioException("Unknown check tool `%s`" % tool) return cls(project_dir, config, envname, options) diff --git a/platformio/check/tools/pvsstudio.py b/platformio/check/tools/pvsstudio.py deleted file mode 100644 index 35d4023f45..0000000000 --- a/platformio/check/tools/pvsstudio.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright (c) 2020-present PlatformIO -# -# 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 = [ - '"%s"' % 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 diff --git a/platformio/dependencies.py b/platformio/dependencies.py index 338f10cf52..0970f7f54b 100644 --- a/platformio/dependencies.py +++ b/platformio/dependencies.py @@ -21,8 +21,7 @@ def get_core_dependencies(): "contrib-pioremote": "~1.0.0", "tool-scons": "~4.40801.0", "tool-cppcheck": "~1.21100.0", - "tool-clangtidy": "~1.150005.0", - "tool-pvs-studio": "~7.18.0", + "tool-clangtidy": "~1.150005.0" } diff --git a/platformio/project/options.py b/platformio/project/options.py index d0a4b0e8e9..1eaa25707e 100644 --- a/platformio/project/options.py +++ b/platformio/project/options.py @@ -617,7 +617,7 @@ def get_default_core_dir(): group="check", name="check_tool", description="A list of check tools used for analysis", - type=click.Choice(["cppcheck", "clangtidy", "pvs-studio"]), + type=click.Choice(["cppcheck", "clangtidy"]), multiple=True, default=["cppcheck"], ), diff --git a/tests/commands/test_check.py b/tests/commands/test_check.py index a022235daf..75e5171b5f 100644 --- a/tests/commands/test_check.py +++ b/tests/commands/test_check.py @@ -62,11 +62,6 @@ """ -PVS_STUDIO_FREE_LICENSE_HEADER = """ -// This is an open source non-commercial project. Dear PVS-Studio, please check it. -// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: http://www.viva64.com -""" - EXPECTED_ERRORS = 5 EXPECTED_WARNINGS = 1 EXPECTED_STYLE = 4 @@ -130,7 +125,7 @@ def test_check_tool_complex_defines_handled( project_dir.join("platformio.ini").write( DEFAULT_CONFIG + R""" -check_tool = cppcheck, clangtidy, pvs-studio +check_tool = cppcheck, clangtidy build_flags = -DEXTERNAL_INCLUDE_FILE=\"test.h\" "-DDEFINE_WITH_SPACE="Hello World!"" @@ -148,8 +143,7 @@ def test_check_tool_complex_defines_handled( ) src_dir.join("main.c").write( - PVS_STUDIO_FREE_LICENSE_HEADER - + """ + """ #if !defined(EXTERNAL_INCLUDE_FILE) #error "EXTERNAL_INCLUDE_FILE is not declared!" #else @@ -315,34 +309,26 @@ def test_check_success_if_no_errors(clirunner, validate_cliresult, tmpdir): def test_check_individual_flags_passed(clirunner, validate_cliresult, tmpdir): - config = DEFAULT_CONFIG + "\ncheck_tool = cppcheck, clangtidy, pvs-studio" + config = DEFAULT_CONFIG + "\ncheck_tool = cppcheck, clangtidy" config += """\ncheck_flags = cppcheck: --std=c++11 clangtidy: --fix-errors - pvs-studio: --analysis-mode=4 """ tmpdir.join("platformio.ini").write(config) - tmpdir.mkdir("src").join("main.cpp").write( - PVS_STUDIO_FREE_LICENSE_HEADER + TEST_CODE - ) + tmpdir.mkdir("src").join("main.cpp").write(TEST_CODE) result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir), "-v"]) validate_cliresult(result) - clang_flags_found = cppcheck_flags_found = pvs_flags_found = False + clang_flags_found = cppcheck_flags_found = False for l in result.output.split("\n"): if "--fix" in l and "clang-tidy" in l and "--std=c++11" not in l: clang_flags_found = True elif "--std=c++11" in l and "cppcheck" in l and "--fix" not in l: cppcheck_flags_found = True - elif ( - "--analysis-mode=4" in l and "pvs-studio" in l.lower() and "--fix" not in l - ): - pvs_flags_found = True assert clang_flags_found assert cppcheck_flags_found - assert pvs_flags_found def test_check_cppcheck_misra_addon(clirunner, validate_cliresult, tmpdir_factory): @@ -440,80 +426,8 @@ def test_check_fails_on_defects_only_on_specified_level( assert low_result.exit_code != 0 -def test_check_pvs_studio_free_license(clirunner, tmpdir): - config = """ -[env:test] -platform = teensy -board = teensy35 -framework = arduino -check_tool = pvs-studio -""" - - tmpdir.join("platformio.ini").write(config) - tmpdir.mkdir("src").join("main.c").write(PVS_STUDIO_FREE_LICENSE_HEADER + TEST_CODE) - - result = clirunner.invoke( - cmd_check, ["--project-dir", str(tmpdir), "--fail-on-defect=high", "-v"] - ) - - errors, warnings, style = count_defects(result.output) - - assert result.exit_code != 0 - assert errors != 0 - assert warnings != 0 - assert style == 0 - - -def test_check_pvs_studio_fails_without_license(clirunner, tmpdir): - config = DEFAULT_CONFIG + "\ncheck_tool = pvs-studio" - - tmpdir.join("platformio.ini").write(config) - tmpdir.mkdir("src").join("main.c").write(TEST_CODE) - - default_result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)]) - verbose_result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir), "-v"]) - - assert default_result.exit_code != 0 - assert "failed to perform check" in default_result.output.lower() - - assert verbose_result.exit_code != 0 - assert "license was not entered" in verbose_result.output.lower() - - -@pytest.mark.skipif( - sys.platform != "win32", - reason="For some reason the error message is different on Windows", -) -def test_check_pvs_studio_fails_broken_license(clirunner, tmpdir): - config = ( - DEFAULT_CONFIG - + """ -check_tool = pvs-studio -check_flags = --lic-file=./pvs-studio.lic -""" - ) - - tmpdir.join("platformio.ini").write(config) - tmpdir.mkdir("src").join("main.c").write(TEST_CODE) - tmpdir.join("pvs-studio.lic").write( - """ -TEST -TEST-TEST-TEST-TEST -""" - ) - - default_result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)]) - verbose_result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir), "-v"]) - - assert default_result.exit_code != 0 - assert "failed to perform check" in default_result.output.lower() - - assert verbose_result.exit_code != 0 - assert "license information is incorrect" in verbose_result.output.lower() - - @pytest.mark.parametrize("framework", ["arduino", "stm32cube", "zephyr"]) -@pytest.mark.parametrize("check_tool", ["cppcheck", "clangtidy", "pvs-studio"]) +@pytest.mark.parametrize("check_tool", ["cppcheck", "clangtidy"]) def test_check_embedded_platform_all_tools( clirunner, validate_cliresult, tmpdir, framework, check_tool ): @@ -525,8 +439,7 @@ def test_check_embedded_platform_all_tools( check_tool = {check_tool} """ tmpdir.mkdir("src").join("main.c").write( - PVS_STUDIO_FREE_LICENSE_HEADER - + """ +""" #include void unused_function(int val){ @@ -612,7 +525,7 @@ def test_check_multiline_error(clirunner, tmpdir_factory): assert verbose_errors == errors == 1 -@pytest.mark.parametrize("check_tool", ["cppcheck", "clangtidy", "pvs-studio"]) +@pytest.mark.parametrize("check_tool", ["cppcheck", "clangtidy"]) def test_check_handles_spaces_in_paths( clirunner, validate_cliresult, tmpdir_factory, check_tool ): @@ -630,9 +543,6 @@ def test_check_handles_spaces_in_paths( check_tool = {check_tool} """ project_dir_with_spaces.join("platformio.ini").write(config) - project_dir_with_spaces.mkdir("src").join("main.cpp").write( - PVS_STUDIO_FREE_LICENSE_HEADER + TEST_CODE - ) result = clirunner.invoke( cmd_check, ["--project-dir", str(project_dir_with_spaces), "-v"] @@ -641,9 +551,7 @@ def test_check_handles_spaces_in_paths( validate_cliresult(result) # Make sure toolchain defines were successfully extracted - if check_tool != "pvs-studio": - # PVS doesn't write defines to stdout - assert "__GNUC__" in result.output + assert "__GNUC__" in result.output #