forked from platformio/platformio-core
Refactor PIO Check feature (#3478)
* Add new option --skip-packages for check command Check tools might fail if they're not able to preprocess source files, for example, Cppcheck uses a custom preprocessor that is not able to parse complex preprocessor code in zephyr framework). Instead user can specify this option to skip headers included from packages and only check project sources. * Fix toolchain built-in include paths order C++ and fixed directories should have higher priority * Refactor check feature The main purpose is to prepare a more comprehensive build environment. It's crucial for cppcheck to be able to check complex frameworks like zephyr, esp-idf, etc. Also detect a special case when cppcheck fails to check the entire project (e.g. a syntax error due to custom preprocessor) * Add new test for check feature Tests ststm32 platform all tools and the main frameworks * Update check tools to the latest available versions * Test check tools and Zephyr framework only with Python 3 * Tidy up code * Add history entry
This commit is contained in:
@ -11,6 +11,7 @@ PlatformIO Core 4
|
|||||||
|
|
||||||
* New `Account Management System <https://docs.platformio.org/page/plus/pio-account.html>`__ with "username" and social providers (preview)
|
* New `Account Management System <https://docs.platformio.org/page/plus/pio-account.html>`__ with "username" and social providers (preview)
|
||||||
* Open source `PIO Remote <http://docs.platformio.org/page/plus/pio-remote.html>`__ client
|
* Open source `PIO Remote <http://docs.platformio.org/page/plus/pio-remote.html>`__ client
|
||||||
|
* Improved `PIO Check <http://docs.platformio.org/page/plus/pio-check.html>`__ with more accurate project processing
|
||||||
* Echo what is typed when ``send_on_enter`` device monitor filter <https://docs.platformio.org/page/projectconf/section_env_monitor.html#monitor-filters>`__ is used (`issue #3452 <https://github.com/platformio/platformio-core/issues/3452>`_)
|
* Echo what is typed when ``send_on_enter`` device monitor filter <https://docs.platformio.org/page/projectconf/section_env_monitor.html#monitor-filters>`__ is used (`issue #3452 <https://github.com/platformio/platformio-core/issues/3452>`_)
|
||||||
* Fixed PIO Unit Testing for Zephyr RTOS
|
* Fixed PIO Unit Testing for Zephyr RTOS
|
||||||
* Fixed UnicodeDecodeError on Windows when network drive (NAS) is used (`issue #3417 <https://github.com/platformio/platformio-core/issues/3417>`_)
|
* Fixed UnicodeDecodeError on Windows when network drive (NAS) is used (`issue #3417 <https://github.com/platformio/platformio-core/issues/3417>`_)
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
VERSION = (4, 3, "2b1")
|
VERSION = (4, 3, "2a4")
|
||||||
__version__ = ".".join([str(s) for s in VERSION])
|
__version__ = ".".join([str(s) for s in VERSION])
|
||||||
|
|
||||||
__title__ = "platformio"
|
__title__ = "platformio"
|
||||||
|
@ -50,10 +50,10 @@ def _dump_includes(env):
|
|||||||
continue
|
continue
|
||||||
toolchain_dir = glob_escape(p.get_package_dir(name))
|
toolchain_dir = glob_escape(p.get_package_dir(name))
|
||||||
toolchain_incglobs = [
|
toolchain_incglobs = [
|
||||||
os.path.join(toolchain_dir, "*", "include*"),
|
|
||||||
os.path.join(toolchain_dir, "*", "include", "c++", "*"),
|
os.path.join(toolchain_dir, "*", "include", "c++", "*"),
|
||||||
os.path.join(toolchain_dir, "*", "include", "c++", "*", "*-*-*"),
|
os.path.join(toolchain_dir, "*", "include", "c++", "*", "*-*-*"),
|
||||||
os.path.join(toolchain_dir, "lib", "gcc", "*", "*", "include*"),
|
os.path.join(toolchain_dir, "lib", "gcc", "*", "*", "include*"),
|
||||||
|
os.path.join(toolchain_dir, "*", "include*"),
|
||||||
]
|
]
|
||||||
for g in toolchain_incglobs:
|
for g in toolchain_incglobs:
|
||||||
includes["toolchain"].extend([os.path.realpath(inc) for inc in glob(g)])
|
includes["toolchain"].extend([os.path.realpath(inc) for inc in glob(g)])
|
||||||
|
@ -61,6 +61,7 @@ from platformio.project.helpers import find_project_dir_above, get_project_dir
|
|||||||
multiple=True,
|
multiple=True,
|
||||||
type=click.Choice(DefectItem.SEVERITY_LABELS.values()),
|
type=click.Choice(DefectItem.SEVERITY_LABELS.values()),
|
||||||
)
|
)
|
||||||
|
@click.option("--skip-packages", is_flag=True)
|
||||||
def cli(
|
def cli(
|
||||||
environment,
|
environment,
|
||||||
project_dir,
|
project_dir,
|
||||||
@ -72,6 +73,7 @@ def cli(
|
|||||||
verbose,
|
verbose,
|
||||||
json_output,
|
json_output,
|
||||||
fail_on_defect,
|
fail_on_defect,
|
||||||
|
skip_packages,
|
||||||
):
|
):
|
||||||
app.set_session_var("custom_project_conf", project_conf)
|
app.set_session_var("custom_project_conf", project_conf)
|
||||||
|
|
||||||
@ -114,6 +116,7 @@ def cli(
|
|||||||
severity=[DefectItem.SEVERITY_LABELS[DefectItem.SEVERITY_HIGH]]
|
severity=[DefectItem.SEVERITY_LABELS[DefectItem.SEVERITY_HIGH]]
|
||||||
if silent
|
if silent
|
||||||
else severity or config.get("env:" + envname, "check_severity"),
|
else severity or config.get("env:" + envname, "check_severity"),
|
||||||
|
skip_packages=skip_packages or env_options.get("check_skip_packages"),
|
||||||
)
|
)
|
||||||
|
|
||||||
for tool in config.get("env:" + envname, "check_tool"):
|
for tool in config.get("env:" + envname, "check_tool"):
|
||||||
@ -222,7 +225,7 @@ def collect_component_stats(result):
|
|||||||
component = dirname(defect.file) or defect.file
|
component = dirname(defect.file) or defect.file
|
||||||
_append_defect(component, defect)
|
_append_defect(component, defect)
|
||||||
|
|
||||||
if component.startswith(get_project_dir()):
|
if component.lower().startswith(get_project_dir().lower()):
|
||||||
while os.sep in component:
|
while os.sep in component:
|
||||||
component = dirname(component)
|
component = dirname(component)
|
||||||
_append_defect(component, defect)
|
_append_defect(component, defect)
|
||||||
|
@ -51,7 +51,7 @@ class DefectItem(object):
|
|||||||
self.cwe = cwe
|
self.cwe = cwe
|
||||||
self.id = id
|
self.id = id
|
||||||
self.file = file
|
self.file = file
|
||||||
if file.startswith(get_project_dir()):
|
if file.lower().startswith(get_project_dir().lower()):
|
||||||
self.file = os.path.relpath(file, get_project_dir())
|
self.file = os.path.relpath(file, get_project_dir())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -14,12 +14,13 @@
|
|||||||
|
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from platformio import fs, proc
|
from platformio import fs, proc
|
||||||
from platformio.commands.check.defect import DefectItem
|
from platformio.commands.check.defect import DefectItem
|
||||||
from platformio.project.helpers import get_project_dir, load_project_ide_data
|
from platformio.project.helpers import load_project_ide_data
|
||||||
|
|
||||||
|
|
||||||
class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
||||||
@ -32,12 +33,13 @@ class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
self.cpp_includes = []
|
self.cpp_includes = []
|
||||||
self.cpp_defines = []
|
self.cpp_defines = []
|
||||||
self.toolchain_defines = []
|
self.toolchain_defines = []
|
||||||
|
self._tmp_files = []
|
||||||
self.cc_path = None
|
self.cc_path = None
|
||||||
self.cxx_path = None
|
self.cxx_path = None
|
||||||
self._defects = []
|
self._defects = []
|
||||||
self._on_defect_callback = None
|
self._on_defect_callback = None
|
||||||
self._bad_input = False
|
self._bad_input = False
|
||||||
self._load_cpp_data(project_dir, envname)
|
self._load_cpp_data(project_dir)
|
||||||
|
|
||||||
# detect all defects by default
|
# detect all defects by default
|
||||||
if not self.options.get("severity"):
|
if not self.options.get("severity"):
|
||||||
@ -52,17 +54,17 @@ class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
for s in self.options["severity"]
|
for s in self.options["severity"]
|
||||||
]
|
]
|
||||||
|
|
||||||
def _load_cpp_data(self, project_dir, envname):
|
def _load_cpp_data(self, project_dir):
|
||||||
data = load_project_ide_data(project_dir, envname)
|
data = load_project_ide_data(project_dir, self.envname)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
self.cc_flags = data.get("cc_flags", "").split(" ")
|
self.cc_flags = click.parser.split_arg_string(data.get("cc_flags", ""))
|
||||||
self.cxx_flags = data.get("cxx_flags", "").split(" ")
|
self.cxx_flags = click.parser.split_arg_string(data.get("cxx_flags", ""))
|
||||||
self.cpp_includes = self._dump_includes(data.get("includes", {}))
|
self.cpp_includes = self._dump_includes(data.get("includes", {}))
|
||||||
self.cpp_defines = data.get("defines", [])
|
self.cpp_defines = data.get("defines", [])
|
||||||
self.cc_path = data.get("cc_path")
|
self.cc_path = data.get("cc_path")
|
||||||
self.cxx_path = data.get("cxx_path")
|
self.cxx_path = data.get("cxx_path")
|
||||||
self.toolchain_defines = self._get_toolchain_defines(self.cc_path)
|
self.toolchain_defines = self._get_toolchain_defines()
|
||||||
|
|
||||||
def get_flags(self, tool):
|
def get_flags(self, tool):
|
||||||
result = []
|
result = []
|
||||||
@ -75,21 +77,43 @@ class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
def _get_toolchain_defines(self):
|
||||||
def _get_toolchain_defines(cc_path):
|
def _extract_defines(language, includes_file):
|
||||||
defines = []
|
build_flags = self.cxx_flags if language == "c++" else self.cc_flags
|
||||||
result = proc.exec_command("echo | %s -dM -E -x c++ -" % cc_path, shell=True)
|
defines = []
|
||||||
|
cmd = "echo | %s -x %s %s %s -dM -E -" % (
|
||||||
|
self.cc_path,
|
||||||
|
language,
|
||||||
|
" ".join([f for f in build_flags if f.startswith(("-m", "-f"))]),
|
||||||
|
includes_file,
|
||||||
|
)
|
||||||
|
result = proc.exec_command(cmd, shell=True)
|
||||||
|
for line in result["out"].split("\n"):
|
||||||
|
tokens = line.strip().split(" ", 2)
|
||||||
|
if not tokens or tokens[0] != "#define":
|
||||||
|
continue
|
||||||
|
if len(tokens) > 2:
|
||||||
|
defines.append("%s=%s" % (tokens[1], tokens[2]))
|
||||||
|
else:
|
||||||
|
defines.append(tokens[1])
|
||||||
|
|
||||||
for line in result["out"].split("\n"):
|
return defines
|
||||||
tokens = line.strip().split(" ", 2)
|
|
||||||
if not tokens or tokens[0] != "#define":
|
|
||||||
continue
|
|
||||||
if len(tokens) > 2:
|
|
||||||
defines.append("%s=%s" % (tokens[1], tokens[2]))
|
|
||||||
else:
|
|
||||||
defines.append(tokens[1])
|
|
||||||
|
|
||||||
return defines
|
incflags_file = self._long_includes_hook(self.cpp_includes)
|
||||||
|
return {lang: _extract_defines(lang, incflags_file) for lang in ("c", "c++")}
|
||||||
|
|
||||||
|
def _create_tmp_file(self, data):
|
||||||
|
with NamedTemporaryFile("w", delete=False) as fp:
|
||||||
|
fp.write(data)
|
||||||
|
self._tmp_files.append(fp.name)
|
||||||
|
return fp.name
|
||||||
|
|
||||||
|
def _long_includes_hook(self, includes):
|
||||||
|
data = []
|
||||||
|
for inc in includes:
|
||||||
|
data.append('-I"%s"' % fs.to_unix_path(inc))
|
||||||
|
|
||||||
|
return '@"%s"' % self._create_tmp_file(" ".join(data))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _dump_includes(includes_map):
|
def _dump_includes(includes_map):
|
||||||
@ -138,18 +162,27 @@ class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
return raw_line
|
return raw_line
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
pass
|
for f in self._tmp_files:
|
||||||
|
if os.path.isfile(f):
|
||||||
|
os.remove(f)
|
||||||
|
|
||||||
def get_project_target_files(self):
|
@staticmethod
|
||||||
allowed_extensions = (".h", ".hpp", ".c", ".cc", ".cpp", ".ino")
|
def get_project_target_files(patterns):
|
||||||
result = []
|
c_extension = (".c",)
|
||||||
|
cpp_extensions = (".cc", ".cpp", ".cxx", ".ino")
|
||||||
|
header_extensions = (".h", ".hh", ".hpp", ".hxx")
|
||||||
|
|
||||||
|
result = {"c": [], "c++": [], "headers": []}
|
||||||
|
|
||||||
def _add_file(path):
|
def _add_file(path):
|
||||||
if not path.endswith(allowed_extensions):
|
if path.endswith(header_extensions):
|
||||||
return
|
result["headers"].append(os.path.realpath(path))
|
||||||
result.append(os.path.realpath(path))
|
elif path.endswith(c_extension):
|
||||||
|
result["c"].append(os.path.realpath(path))
|
||||||
|
elif path.endswith(cpp_extensions):
|
||||||
|
result["c++"].append(os.path.realpath(path))
|
||||||
|
|
||||||
for pattern in self.options["patterns"]:
|
for pattern in patterns:
|
||||||
for item in glob.glob(pattern):
|
for item in glob.glob(pattern):
|
||||||
if not os.path.isdir(item):
|
if not os.path.isdir(item):
|
||||||
_add_file(item)
|
_add_file(item)
|
||||||
@ -159,27 +192,23 @@ class CheckToolBase(object): # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_source_language(self):
|
|
||||||
with fs.cd(get_project_dir()):
|
|
||||||
for _, __, files in os.walk(self.config.get_optional_dir("src")):
|
|
||||||
for name in files:
|
|
||||||
if "." not in name:
|
|
||||||
continue
|
|
||||||
if os.path.splitext(name)[1].lower() in (".cpp", ".cxx", ".ino"):
|
|
||||||
return "c++"
|
|
||||||
return "c"
|
|
||||||
|
|
||||||
def check(self, on_defect_callback=None):
|
def check(self, on_defect_callback=None):
|
||||||
self._on_defect_callback = on_defect_callback
|
self._on_defect_callback = on_defect_callback
|
||||||
cmd = self.configure_command()
|
cmd = self.configure_command()
|
||||||
if self.options.get("verbose"):
|
if cmd:
|
||||||
click.echo(" ".join(cmd))
|
if self.options.get("verbose"):
|
||||||
|
click.echo(" ".join(cmd))
|
||||||
|
|
||||||
proc.exec_command(
|
proc.exec_command(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
stdout=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
||||||
stderr=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
stderr=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self.options.get("verbose"):
|
||||||
|
click.echo("Error: Couldn't configure command")
|
||||||
|
self._bad_input = True
|
||||||
|
|
||||||
self.clean_up()
|
self.clean_up()
|
||||||
|
|
||||||
|
@ -57,11 +57,28 @@ class ClangtidyCheckTool(CheckToolBase):
|
|||||||
if not self.is_flag_set("--checks", flags):
|
if not self.is_flag_set("--checks", flags):
|
||||||
cmd.append("--checks=*")
|
cmd.append("--checks=*")
|
||||||
|
|
||||||
|
project_files = self.get_project_target_files(self.options["patterns"])
|
||||||
|
|
||||||
|
src_files = []
|
||||||
|
for scope in project_files:
|
||||||
|
src_files.extend(project_files[scope])
|
||||||
|
|
||||||
cmd.extend(flags)
|
cmd.extend(flags)
|
||||||
cmd.extend(self.get_project_target_files())
|
cmd.extend(src_files)
|
||||||
cmd.append("--")
|
cmd.append("--")
|
||||||
|
|
||||||
cmd.extend(["-D%s" % d for d in self.cpp_defines + self.toolchain_defines])
|
cmd.extend(
|
||||||
cmd.extend(["-I%s" % inc for inc in self.cpp_includes])
|
["-D%s" % d for d in self.cpp_defines + self.toolchain_defines["c++"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
includes = []
|
||||||
|
for inc in self.cpp_includes:
|
||||||
|
if self.options.get("skip_packages") and inc.lower().startswith(
|
||||||
|
self.config.get_optional_dir("packages").lower()
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
includes.append(inc)
|
||||||
|
|
||||||
|
cmd.append("--extra-arg=" + self._long_includes_hook(includes))
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -12,10 +12,11 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from os import remove
|
import os
|
||||||
from os.path import isfile, join
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from platformio import proc
|
||||||
from platformio.commands.check.defect import DefectItem
|
from platformio.commands.check.defect import DefectItem
|
||||||
from platformio.commands.check.tools.base import CheckToolBase
|
from platformio.commands.check.tools.base import CheckToolBase
|
||||||
from platformio.managers.core import get_core_package_dir
|
from platformio.managers.core import get_core_package_dir
|
||||||
@ -23,7 +24,6 @@ from platformio.managers.core import get_core_package_dir
|
|||||||
|
|
||||||
class CppcheckCheckTool(CheckToolBase):
|
class CppcheckCheckTool(CheckToolBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._tmp_files = []
|
|
||||||
self.defect_fields = [
|
self.defect_fields = [
|
||||||
"severity",
|
"severity",
|
||||||
"message",
|
"message",
|
||||||
@ -74,10 +74,32 @@ class CppcheckCheckTool(CheckToolBase):
|
|||||||
else:
|
else:
|
||||||
args["severity"] = DefectItem.SEVERITY_LOW
|
args["severity"] = DefectItem.SEVERITY_LOW
|
||||||
|
|
||||||
|
# Skip defects found in third-party software, but keep in mind that such defects
|
||||||
|
# might break checking process so defects from project files are not reported
|
||||||
|
breaking_defect_ids = ("preprocessorErrorDirective", "syntaxError")
|
||||||
|
if (
|
||||||
|
args.get("file", "")
|
||||||
|
.lower()
|
||||||
|
.startswith(self.config.get_optional_dir("packages").lower())
|
||||||
|
):
|
||||||
|
if args["id"] in breaking_defect_ids:
|
||||||
|
if self.options.get("verbose"):
|
||||||
|
click.echo(
|
||||||
|
"Error: Found a breaking defect '%s' in %s:%s\n"
|
||||||
|
"Please note: check results might not be valid!\n"
|
||||||
|
"Try adding --skip-packages"
|
||||||
|
% (args.get("message"), args.get("file"), args.get("line"))
|
||||||
|
)
|
||||||
|
click.echo()
|
||||||
|
self._bad_input = True
|
||||||
|
return None
|
||||||
|
|
||||||
return DefectItem(**args)
|
return DefectItem(**args)
|
||||||
|
|
||||||
def configure_command(self):
|
def configure_command(
|
||||||
tool_path = join(get_core_package_dir("tool-cppcheck"), "cppcheck")
|
self, language, src_files
|
||||||
|
): # pylint: disable=arguments-differ
|
||||||
|
tool_path = os.path.join(get_core_package_dir("tool-cppcheck"), "cppcheck")
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
tool_path,
|
tool_path,
|
||||||
@ -108,51 +130,112 @@ class CppcheckCheckTool(CheckToolBase):
|
|||||||
cmd.append("--enable=%s" % ",".join(enabled_checks))
|
cmd.append("--enable=%s" % ",".join(enabled_checks))
|
||||||
|
|
||||||
if not self.is_flag_set("--language", flags):
|
if not self.is_flag_set("--language", flags):
|
||||||
if self.get_source_language() == "c++":
|
cmd.append("--language=" + language)
|
||||||
cmd.append("--language=c++")
|
|
||||||
|
|
||||||
if not self.is_flag_set("--std", flags):
|
build_flags = self.cxx_flags if language == "c++" else self.cc_flags
|
||||||
for f in self.cxx_flags + self.cc_flags:
|
|
||||||
if "-std" in f:
|
for flag in build_flags:
|
||||||
# Standards with GNU extensions are not allowed
|
if "-std" in flag:
|
||||||
cmd.append("-" + f.replace("gnu", "c"))
|
# Standards with GNU extensions are not allowed
|
||||||
|
cmd.append("-" + flag.replace("gnu", "c"))
|
||||||
|
|
||||||
|
cmd.extend(
|
||||||
|
["-D%s" % d for d in self.cpp_defines + self.toolchain_defines[language]]
|
||||||
|
)
|
||||||
|
|
||||||
cmd.extend(["-D%s" % d for d in self.cpp_defines + self.toolchain_defines])
|
|
||||||
cmd.extend(flags)
|
cmd.extend(flags)
|
||||||
|
|
||||||
cmd.append("--file-list=%s" % self._generate_src_file())
|
cmd.extend(
|
||||||
|
"--include=" + inc
|
||||||
|
for inc in self.get_forced_includes(build_flags, self.cpp_includes)
|
||||||
|
)
|
||||||
|
cmd.append("--file-list=%s" % self._generate_src_file(src_files))
|
||||||
cmd.append("--includes-file=%s" % self._generate_inc_file())
|
cmd.append("--includes-file=%s" % self._generate_inc_file())
|
||||||
|
|
||||||
core_dir = self.config.get_optional_dir("packages")
|
|
||||||
cmd.append("--suppress=*:%s*" % core_dir)
|
|
||||||
cmd.append("--suppress=unmatchedSuppression:%s*" % core_dir)
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def _create_tmp_file(self, data):
|
@staticmethod
|
||||||
with NamedTemporaryFile("w", delete=False) as fp:
|
def get_forced_includes(build_flags, includes):
|
||||||
fp.write(data)
|
def _extract_filepath(flag, include_options, build_flags):
|
||||||
self._tmp_files.append(fp.name)
|
path = ""
|
||||||
return fp.name
|
for option in include_options:
|
||||||
|
if not flag.startswith(option):
|
||||||
|
continue
|
||||||
|
if flag.split(option)[1].strip():
|
||||||
|
path = flag.split(option)[1].strip()
|
||||||
|
elif build_flags.index(flag) + 1 < len(build_flags):
|
||||||
|
path = build_flags[build_flags.index(flag) + 1]
|
||||||
|
return path
|
||||||
|
|
||||||
def _generate_src_file(self):
|
def _search_include_dir(filepath, include_paths):
|
||||||
src_files = [
|
for inc_path in include_paths:
|
||||||
f for f in self.get_project_target_files() if not f.endswith((".h", ".hpp"))
|
path = os.path.join(inc_path, filepath)
|
||||||
]
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
return ""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
include_options = ("-include", "-imacros")
|
||||||
|
for f in build_flags:
|
||||||
|
if f.startswith(include_options):
|
||||||
|
filepath = _extract_filepath(f, include_options, build_flags)
|
||||||
|
if not os.path.isabs(filepath):
|
||||||
|
filepath = _search_include_dir(filepath, includes)
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
result.append(filepath)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _generate_src_file(self, src_files):
|
||||||
return self._create_tmp_file("\n".join(src_files))
|
return self._create_tmp_file("\n".join(src_files))
|
||||||
|
|
||||||
def _generate_inc_file(self):
|
def _generate_inc_file(self):
|
||||||
return self._create_tmp_file("\n".join(self.cpp_includes))
|
result = []
|
||||||
|
for inc in self.cpp_includes:
|
||||||
|
if self.options.get("skip_packages") and inc.lower().startswith(
|
||||||
|
self.config.get_optional_dir("packages").lower()
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
result.append(inc)
|
||||||
|
return self._create_tmp_file("\n".join(result))
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
for f in self._tmp_files:
|
super(CppcheckCheckTool, self).clean_up()
|
||||||
if isfile(f):
|
|
||||||
remove(f)
|
|
||||||
|
|
||||||
# delete temporary dump files generated by addons
|
# delete temporary dump files generated by addons
|
||||||
if not self.is_flag_set("--addon", self.get_flags("cppcheck")):
|
if not self.is_flag_set("--addon", self.get_flags("cppcheck")):
|
||||||
return
|
return
|
||||||
for f in self.get_project_target_files():
|
|
||||||
dump_file = f + ".dump"
|
for files in self.get_project_target_files(self.options["patterns"]).values():
|
||||||
if isfile(dump_file):
|
for f in files:
|
||||||
remove(dump_file)
|
dump_file = f + ".dump"
|
||||||
|
if os.path.isfile(dump_file):
|
||||||
|
os.remove(dump_file)
|
||||||
|
|
||||||
|
def check(self, on_defect_callback=None):
|
||||||
|
self._on_defect_callback = on_defect_callback
|
||||||
|
project_files = self.get_project_target_files(self.options["patterns"])
|
||||||
|
|
||||||
|
languages = ("c", "c++")
|
||||||
|
if not any([project_files[t] for t in languages]):
|
||||||
|
click.echo("Error: Nothing to check.")
|
||||||
|
return True
|
||||||
|
for language in languages:
|
||||||
|
if not project_files[language]:
|
||||||
|
continue
|
||||||
|
cmd = self.configure_command(language, project_files[language])
|
||||||
|
if not cmd:
|
||||||
|
self._bad_input = True
|
||||||
|
continue
|
||||||
|
if self.options.get("verbose"):
|
||||||
|
click.echo(" ".join(cmd))
|
||||||
|
|
||||||
|
proc.exec_command(
|
||||||
|
cmd,
|
||||||
|
stdout=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
||||||
|
stderr=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.clean_up()
|
||||||
|
|
||||||
|
return self._bad_input
|
||||||
|
@ -199,32 +199,37 @@ class PvsStudioCheckTool(CheckToolBase): # pylint: disable=too-many-instance-at
|
|||||||
self._bad_input = True
|
self._bad_input = True
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
|
super(PvsStudioCheckTool, self).clean_up()
|
||||||
if os.path.isdir(self._tmp_dir):
|
if os.path.isdir(self._tmp_dir):
|
||||||
shutil.rmtree(self._tmp_dir)
|
shutil.rmtree(self._tmp_dir)
|
||||||
|
|
||||||
def check(self, on_defect_callback=None):
|
def check(self, on_defect_callback=None):
|
||||||
self._on_defect_callback = on_defect_callback
|
self._on_defect_callback = on_defect_callback
|
||||||
src_files = [
|
for scope, files in self.get_project_target_files(
|
||||||
f for f in self.get_project_target_files() if not f.endswith((".h", ".hpp"))
|
self.options["patterns"]
|
||||||
]
|
).items():
|
||||||
|
if scope not in ("c", "c++"):
|
||||||
for src_file in src_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
|
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 = proc.exec_command(cmd)
|
result = proc.exec_command(cmd)
|
||||||
# pylint: disable=unsupported-membership-test
|
# pylint: disable=unsupported-membership-test
|
||||||
if result["returncode"] != 0 or "License was not entered" in result["err"]:
|
if (
|
||||||
self._bad_input = True
|
result["returncode"] != 0
|
||||||
click.echo(result["err"])
|
or "license" in result["err"].lower()
|
||||||
continue
|
):
|
||||||
|
self._bad_input = True
|
||||||
|
click.echo(result["err"])
|
||||||
|
continue
|
||||||
|
|
||||||
self._process_defects(self.parse_defects(self._tmp_output_file))
|
self._process_defects(self.parse_defects(self._tmp_output_file))
|
||||||
|
|
||||||
self.clean_up()
|
self.clean_up()
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from platformio.commands.device import DeviceMonitorFilter
|
from platformio.commands.device import DeviceMonitorFilter
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +23,6 @@ class SendOnEnter(DeviceMonitorFilter):
|
|||||||
self._buffer = ""
|
self._buffer = ""
|
||||||
|
|
||||||
def tx(self, text):
|
def tx(self, text):
|
||||||
click.echo(text, nl=False)
|
|
||||||
self._buffer += text
|
self._buffer += text
|
||||||
if self._buffer.endswith("\r\n"):
|
if self._buffer.endswith("\r\n"):
|
||||||
text = self._buffer[:-2]
|
text = self._buffer[:-2]
|
||||||
|
@ -28,9 +28,9 @@ CORE_PACKAGES = {
|
|||||||
"contrib-pysite": "~2.%d%d.0" % (sys.version_info.major, sys.version_info.minor),
|
"contrib-pysite": "~2.%d%d.0" % (sys.version_info.major, sys.version_info.minor),
|
||||||
"tool-unity": "~1.20500.0",
|
"tool-unity": "~1.20500.0",
|
||||||
"tool-scons": "~2.20501.7" if PY2 else "~3.30102.0",
|
"tool-scons": "~2.20501.7" if PY2 else "~3.30102.0",
|
||||||
"tool-cppcheck": "~1.189.0",
|
"tool-cppcheck": "~1.190.0",
|
||||||
"tool-clangtidy": "^1.80000.0",
|
"tool-clangtidy": "~1.100000.0",
|
||||||
"tool-pvs-studio": "~7.5.0",
|
"tool-pvs-studio": "~7.7.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ
|
||||||
|
@ -566,6 +566,13 @@ ProjectOptions = OrderedDict(
|
|||||||
type=click.Choice(["low", "medium", "high"]),
|
type=click.Choice(["low", "medium", "high"]),
|
||||||
default=["low", "medium", "high"],
|
default=["low", "medium", "high"],
|
||||||
),
|
),
|
||||||
|
ConfigEnvOption(
|
||||||
|
group="check",
|
||||||
|
name="check_skip_packages",
|
||||||
|
description="Skip checking includes from packages directory",
|
||||||
|
type=click.BOOL,
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
# Test
|
# Test
|
||||||
ConfigEnvOption(
|
ConfigEnvOption(
|
||||||
group="test",
|
group="test",
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -383,3 +384,47 @@ check_tool = pvs-studio
|
|||||||
assert errors != 0
|
assert errors != 0
|
||||||
assert warnings != 0
|
assert warnings != 0
|
||||||
assert style == 0
|
assert style == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_embedded_platform_all_tools(clirunner, tmpdir):
|
||||||
|
config = """
|
||||||
|
[env:test]
|
||||||
|
platform = ststm32
|
||||||
|
board = nucleo_f401re
|
||||||
|
framework = %s
|
||||||
|
check_tool = %s
|
||||||
|
"""
|
||||||
|
# tmpdir.join("platformio.ini").write(config)
|
||||||
|
tmpdir.mkdir("src").join("main.c").write(
|
||||||
|
"""// 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
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void unused_function(int val){
|
||||||
|
int unusedVar = 0;
|
||||||
|
int* iP = &unusedVar;
|
||||||
|
*iP++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
frameworks = ["arduino", "mbed", "stm32cube"]
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
# Zephyr only supports Python 3
|
||||||
|
frameworks.append("zephyr")
|
||||||
|
|
||||||
|
for framework in frameworks:
|
||||||
|
for tool in ("cppcheck", "clangtidy", "pvs-studio"):
|
||||||
|
tmpdir.join("platformio.ini").write(config % (framework, tool))
|
||||||
|
|
||||||
|
result = clirunner.invoke(cmd_check, ["--project-dir", str(tmpdir)])
|
||||||
|
|
||||||
|
defects = sum(count_defects(result.output))
|
||||||
|
|
||||||
|
assert result.exit_code == 0 and defects > 0, "Failed %s with %s" % (
|
||||||
|
framework,
|
||||||
|
tool,
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user