Source code for statick_tool.tool_plugin
"""Tool plugin."""
import argparse
import logging
import os
import shlex
from typing import Any, Dict, List, Optional, Union
from yapsy.IPlugin import IPlugin
from statick_tool.issue import Issue
from statick_tool.package import Package
from statick_tool.plugin_context import PluginContext
# No stubs available for IPlugin so ignoring type.
[docs]class ToolPlugin(IPlugin): # type: ignore
"""Default implementation of tool plugin."""
plugin_context = None
[docs] def get_name(self) -> str: # type: ignore[empty-body]
"""Get name of tool."""
pass # pylint: disable=unnecessary-pass
[docs] @classmethod
def get_tool_dependencies(cls) -> List[str]:
"""Get a list of tools that must run before this one."""
return []
[docs] def get_file_types(self) -> List[str]: # type: ignore[empty-body]
"""Return a list of file types the plugin can scan."""
[docs] def scan(self, package: Package, level: str) -> Optional[List[Issue]]:
"""Run tool and gather output."""
files: List[str] = []
for file_type in self.get_file_types():
if file_type in package and package[file_type]:
files += package[file_type]
if files:
total_output = ( # pylint: disable=assignment-from-no-return
self.process_files(package, level, files, self.get_user_flags(level))
)
if total_output is not None:
if self.plugin_context and self.plugin_context.args.output_directory:
with open(self.get_name() + ".log", "w", encoding="utf8") as fid:
for output in total_output:
fid.write(output)
return self.parse_output(total_output, package)
return None
return []
[docs] def process_files(
self, package: Package, level: str, files: List[str], user_flags: List[str]
) -> Optional[List[str]]:
"""Run tool and gather output."""
[docs] def parse_output( # type: ignore[empty-body]
self, total_output: List[str], package: Optional[Package] = None
) -> List[Issue]:
"""Parse tool output and report issues."""
[docs] def set_plugin_context(self, plugin_context: Union[None, PluginContext]) -> None:
"""Set the plugin context."""
self.plugin_context = plugin_context
[docs] def load_mapping(self) -> Dict[str, str]:
"""Load a mapping between warnings and identifiers."""
file_name: str = f"plugin_mapping/{self.get_name()}.txt"
assert self.plugin_context is not None
full_path: Union[Any, str, None] = self.plugin_context.resources.get_file(
file_name
)
if (
"mapping_file_suffix" in self.plugin_context.args
and self.plugin_context.args.mapping_file_suffix is not None
):
# If the user specified a suffix, try to get the suffixed version of the
# file.
suffixed_file_name = (
f"plugin_mapping/{self.get_name()}-"
f"{self.plugin_context.args.mapping_file_suffix}.txt"
)
suffixed_full_path = self.plugin_context.resources.get_file(
suffixed_file_name
)
if suffixed_full_path is not None:
# If there actually is a file with that suffix, use it.
# Else use the un-suffixed version.
full_path = suffixed_full_path
if full_path is None:
return {}
warning_mapping: Dict[str, str] = {}
with open(full_path, "r", encoding="utf8") as mapping_file:
for line in mapping_file.readlines():
split_line = line.strip().split(":")
if len(split_line) != 2:
logging.warning(
"Invalid line %s in mapping file %s", line, file_name
)
continue
warning_mapping[split_line[0]] = split_line[1]
return warning_mapping
[docs] def get_user_flags(self, level: str, name: Optional[str] = None) -> List[str]:
"""Get the user-defined extra flags for a specific tool/level combination."""
if name is None:
name = self.get_name() # pylint: disable=assignment-from-no-return
assert self.plugin_context is not None
user_flags = self.plugin_context.config.get_tool_config(name, level, "flags")
flags: List[str] = []
if user_flags:
# See https://github.com/python/typeshed/issues/1476 for
# justification to ignore.
lex = shlex.shlex(user_flags, posix=True)
lex.whitespace_split = True
flags = list(lex)
return flags
[docs] @staticmethod
def is_valid_executable(path: str) -> bool:
"""Return whether a provided command exists and is executable.
If the provided path has an extension on it, don't change it, otherwise try
adding common extensions.
"""
# On Windows, PATHEXT contains a list of extensions which can be
# appended to a program name when searching PATH.
extensions = os.environ.get("PATHEXT", None)
_, path_ext = os.path.splitext(path)
if path_ext or not extensions:
return os.path.isfile(path) and os.access(path, os.X_OK)
extensions_list = extensions.split(";")
# Add "" (no extension) as a possibility.
extensions_list.insert(0, "")
for ext in extensions_list:
extended_path = path + ext
if os.path.isfile(extended_path) and os.access(extended_path, os.X_OK):
return True
return False
[docs] @staticmethod
def command_exists(command: str) -> bool:
"""Return whether a particular command is available on $PATH."""
fpath, _ = os.path.split(command)
if fpath:
# Contains a path, not just a command, so don't search PATH
return ToolPlugin.is_valid_executable(command)
for path in os.environ["PATH"].split(os.pathsep):
exe_path = os.path.join(path, command)
if ToolPlugin.is_valid_executable(exe_path):
return True
return False