Source code for gnatcheck

# GNAThub (GNATdashboard)
# Copyright (C) 2013-2020, AdaCore
#
# This is free software;  you can redistribute it  and/or modify it  under
# terms of the  GNU General Public License as published  by the Free Soft-
# ware  Foundation;  either version 3,  or (at your option) any later ver-
# sion.  This software is distributed in the hope  that it will be useful,
# but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
# TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for  more details.  You should have  received  a copy of the GNU
# General  Public  License  distributed  with  this  software;   see  file
# COPYING3.  If not, go to http://www.gnu.org/licenses for a complete copy
# of the license.

"""GNAThub plug-in for the GNATcheck command-line tool.

It exports the GNATcheck class which implements the :class:`GNAThub.Plugin`
interface. This allows GNAThub's plug-in scanner to automatically find this
module and load it as part of the GNAThub default execution.
"""

import collections
import os
import re
import shutil

from _gnat import SLOC_PATTERN

import GNAThub
from GNAThub import Console, Plugin, Reporter, Runner


[docs]class GNATcheck(Plugin, Runner, Reporter): """GNATcheck plugin for GNAThub. Configures and executes GNATcheck, then analyzes the output. """ # Regex to identify lines that contain sections titles ''' Pattern to match common title like: 1. Summary Format is [section_nb]. [section_title] ''' _SECTION_TITLE_PATTERN = r'(?P<snumber>\d+(?:\.\d+)*)\. (?P<stitle>\w+)' # Regex to identify lines that contain messages ''' Pattern to match common rules like: p.adb:45:12: positional parameter association [Positional_Parameters] Format is [SLOC] [message] [rule_id] ''' _RULE_PATTERN = r'(?P<message>.+)\s\[(?P<rule_id>[A-Za-z0-9_|: ]+)\]$' ''' Pattern to match specific rules that are checked inside expanded generic instantiations p_g.ads:5:13: function returns unconstrained array [instance at p.ads:12] [unconstrained_array_returns] Format is [SLOC] [message] [instance] [rule_id] ''' _RULE_PATTERN_INST = \ (r'((?P<message>.+)\s\[(?P<instance>[a-zA-Z-_.0-9 ]+:' r'([0-9]+)))\]\s\[(?P<rule_id>[A-Za-z0-9_| ]+):?' r'(?P<rule_info>[A-Za-z0..9_:| ]+)?\]$') # Regular expression to match GNATcheck output and extract all relevant # information stored in it. _TITLE = re.compile(r'%s' % (_SECTION_TITLE_PATTERN)) _MESSAGE = re.compile(r'%s:\s%s' % (SLOC_PATTERN, _RULE_PATTERN)) _MESSAGE_INST = re.compile( r'%s:?\s%s' % (SLOC_PATTERN, _RULE_PATTERN_INST)) # GNATcheck exits with an error code of 1 even on a successful run VALID_EXIT_CODES = (0, 1)
[docs] def __init__(self): super(GNATcheck, self).__init__() if GNAThub.dry_run_without_project(): return self.tool = None self.output = os.path.join(GNAThub.Project.artifacts_dir(), '%s.out' % self.name) # Map of rules (couple (name, rule): dict[str,Rule]) self.rules = {} # Map of messages (couple (rule, message): dict[str,Message]) self.messages = {} # Map of bulk data (couple (source, message_data): dict[str,list]) self.bulk_data = collections.defaultdict(list)
def __cmd_exists(self, cmd): return shutil.which(cmd) is not None def __cmd_line(self): """Create GNATcheck command line arguments list. :return: the GNATcheck command line :rtype: collections.Iterable[str] """ cmd_line = [ 'gnatcheck', '--show-rule', '-o', self.output, '-P', GNAThub.Project.path()] if GNAThub.u_process_all(): cmd_line.extend(['-U']) # Keeping this for later implemntation of -U main switch # if GNAThub.u_main(): # cmd_line.extend(['-U']) # cmd_line.extend([GNAThub.u_main()]) cmd_line.extend(['-j%d' % GNAThub.jobs()]) cmd_line = cmd_line + GNAThub.Project.scenario_switches() if GNAThub.Project.target(): cmd = '{}-{}'.format(GNAThub.Project.target(), cmd_line[0]) if self.__cmd_exists(cmd): cmd_line[0] = cmd else: cmd_line.extend(['--target', GNAThub.Project.target()]) if GNAThub.Project.runtime(): cmd_line.extend(('--RTS', GNAThub.Project.runtime())) if GNAThub.subdirs(): cmd_line.extend(['--subdirs=' + GNAThub.subdirs()]) return cmd_line
[docs] def run(self): """Execute GNATcheck. Returns according to the success of the execution of the tool: * ``GNAThub.EXEC_SUCCESS``: on successful execution * ``GNAThub.EXEC_FAILURE``: on any error """ status = GNAThub.Run(self.name, self.__cmd_line()).status # the GNATcheck analysis results can be trusted only for 0 and 1 exit # codes, for the other values (2, 3, 4, 5 and 6) an execution failure # is set if status in GNATcheck.VALID_EXIT_CODES: return GNAThub.EXEC_SUCCESS else: # handle the different GNATcheck exit codes status_str = str(status) if status == 2: msg = 'A tool failure was detected' elif status == 3: msg = 'No Ada source file was checked' elif status == 4: msg = 'Parameter of the rule -from denotes a nonexistent file' elif status == 5: msg = 'The name of an unknown rule in a rule option' msg = msg + ' or some problem with rule parameters' elif status == 6: msg = 'An issue with the rules specification' else: msg = 'Unexpected exit code' self.error('GNATcheck Execution exit code %s: %s', status_str, msg) return GNAThub.EXEC_FAILURE
[docs] def report(self): """Parse GNATcheck output file report. Returns according to the success of the analysis: * ``GNAThub.EXEC_SUCCESS``: on successful execution and analysis * ``GNAThub.EXEC_FAILURE``: on any error Identify two type of messages with different format: * basic message * message for package instantiation """ # Clear existing references only if not incremental run if not GNAThub.incremental(): self.info('clear existing results if any') GNAThub.Tool.clear_references(self.name) self.info('analyse report') self.tool = GNAThub.Tool(self.name) self.log.debug('parse report: %s', self.output) if not os.path.exists(self.output): self.error('no report found') return GNAThub.EXEC_FAILURE try: gnatcheck_problems = False with open(self.output, 'r') as output: lines = output.readlines() total = len(lines) # Local variables used for exemptions handling exempted_violation = False hide_exempted = GNAThub.gnatcheck_hide_exempted() # Add the tag "exempted" for GNATcheck exempted violations exempt_tag = GNAThub.Property('gnatcheck:exempted', 'Exempted') prev_line = "" for index, line in enumerate(lines, start=1): self.log.debug('parse line: %s', line) # check if is a section title matchTitle = self._TITLE.match(line) if matchTitle: stitle = matchTitle.group('stitle') exempted_violation = stitle in ('Exempted', 'EXEMPTED') # create filtering for the messages reported in the # section # 6. Gnatcheck internal errors # since these internal gnatcheck errors gnatcheck_problems = matchTitle.group('snumber') == '6' # filter messages if occurs in exempted violation section import_violation = not exempted_violation or ( exempted_violation and not hide_exempted) handle_exempted = exempted_violation and not hide_exempted if import_violation and not gnatcheck_problems: if handle_exempted: match1 = self._MESSAGE.match(line) if match1: self.log.debug('matched: %s', str(match1.groups())) # Store this line in order to gather next line # justification if any if prev_line == "": prev_line = line else: # Second line is a new violation report match_prev = self._MESSAGE.match(prev_line) if match_prev: self.__parse_line_exempted( match_prev, [exempt_tag]) prev_line = line # self.__parse_line_exempted(match1, # [exempt_tag]) else: if prev_line != "": if len(line.strip()) != 0: # Handle justification for prev_line pmatch = self._MESSAGE.match(prev_line) if pmatch: self.__parse_line_exempted( pmatch, [exempt_tag], line.strip()) # Reset previous line value prev_line = "" else: match2 = self._MESSAGE_INST.match(line) if match2: self.log.debug('matched inst: %s', str(match2.groups())) self.__parse_line_inst(match2) else: match = self._MESSAGE.match(line) if match: self.log.debug('matched : %s', str(match.groups())) self.__parse_line(match) Console.progress(index, total, new_line=(index == total)) except IOError as why: self.log.exception('failed to parse report') self.error('%s (%s:%d)' % ( why, os.path.basename(self.output), total)) return GNAThub.EXEC_FAILURE else: self.__do_bulk_insert() return GNAThub.EXEC_SUCCESS
def __parse_line(self, regex): """Parse a GNATcheck message line. Adds the message to the current database session. Retrieves following information: * source basename * line in source * rule identification * message description :param re.RegexObject regex: the result of the _MESSAGE regex """ # The following Regex results are explained using this example. # 'input.adb:3:19: use clause for package [USE_PACKAGE_Clauses]' # Extract each component from the message: # ('input.adb', '3', '19', 'use clause for package', # 'USE_PACKAGE_Clauses') base = regex.group('file') src = GNAThub.Project.source_file(base) line = regex.group('line') column = regex.group('column') message = regex.group('message') rule_id = regex.group('rule_id').lower() # This is to add support for custom rule names and the generated rule # id when the gnatcheck text output has the form # [custom_rule_name|rule_id] # (i.e., something like # summaries.adb:219:05: silent exception handler # [SR266|silent_exception_handler]) # In order to match with default gnatcheck rules, the rule id is kept # as the default rule's name and the custome rule name related # information is added as prefix of the associated message. # This allows compatibility with sonar-scanner and allows to upload # these kind of violation into the SonarQube dashboard. custom_rule_name = '' if '|' in rule_id: custom_rule_name, rule_id = rule_id.strip().split('|', 1) message = '[' + custom_rule_name + '] ' + message # This is to add support for warnings, style-checks, ... that can have # the rule id as [rule_id:rule_switch] # (i.e., something like # f.adb:10:07: variable "uninitialized" is read but never assigned # [warnings:v]) rule_switch = '' if ':' in rule_id: rule_id, rule_switch = rule_id.strip().split(':', 1) message = '[' + rule_id + ':' + rule_switch + '] ' + message self.__add_message(src, line, column, rule_id, message) def __parse_line_exempted(self, regex, tag, justify=""): """Parse a GNATcheck exempted violation message line. Adds the message to the current database session. Retrieves following information: * source basename * line in source * rule identification * message description :param re.RegexObject regex: the result of the _MESSAGE regex :param tag: the message properties :type tag: collections.Iterable[GNAThub.Property] or None :param justify: the exemption associated justification string or None """ # The following Regex results are explained using this example. # 'input.adb:3:19: use clause for package [USE_PACKAGE_Clauses]' # Extract each component from the message: # ('input.adb', '3', '19', 'use clause for package', # 'USE_PACKAGE_Clauses') base = regex.group('file') src = GNAThub.Project.source_file(base) line = regex.group('line') column = regex.group('column') message = regex.group('message') rule_id = regex.group('rule_id').lower() # This is to add support for custom rule names and the generated # rule id when the gnatcheck text output has the form # [custom_rule_name|rule_id] # In order to match with default gnatcheck rules, the rule id is kept # as the default rule's name and the custome rule name related # information is added as prefix of the associated message. # This allows compatibility with sonar-scanner and allows to upload # these kind of violation into the SonarQube dashboard. custom_rule_name = '' if '|' in rule_id: custom_rule_name, rule_id = rule_id.strip().split('|', 1) message = '[' + custom_rule_name + '] ' + message # This is to add support for warnings, style-checks, ... that can have # the rule id as [rule_id:rule_switch] # (i.e., something like # f.adb:10:07: variable "uninitialized" is read but never assigned # [warnings:v]) rule_switch = '' if ':' in rule_id: rule_id, rule_switch = rule_id.strip().split(':', 1) message = '[' + rule_id + ':' + rule_switch + '] ' + message # Add justification as part of the associated message new_message = message if justify != "": new_message = message + ' ' + justify self.__add_message(src, line, column, rule_id, new_message, tag) def __parse_line_inst(self, regex): """Parse a GNATcheck instance message line. Adds the formatted instance message to the current database session. Retrieves following information: * source basename * line in source * rule identification * instance + message description :param re.RegexObject regex: the result of the _MESSAGE_INST regex """ # The following Regex results are explained using this example. # p_g.ads:5:13: function returns unconstrained array # [instance at p.ads:12] [unconstrained_array_returns] # Extract each component from the message: # ('p_g.ads', '5', '13', 'function returns unconstrained array', # 'instance at p.ads:12', 'Unconstrained_Array_Returns') base = regex.group('file') src = GNAThub.Project.source_file(base) line = regex.group('line') column = regex.group('column') instance = regex.group('instance') message = regex.group('message') rule_id = regex.group('rule_id').lower() # This is to add support for custom rule names and the generated rule # id when the gnatcheck text output has the form # [custom_rule_name|rule_id] # In order to match with default gnatcheck rules, the rule id is kept # as the default rule's name and the custome rule name related # information is added as prefix of the associated message. # This allows compatibility with sonar-scanner and allows to upload # these kind of violation into the SonarQube dashboard. custom_rule_name = '' if '|' in rule_id: custom_rule_name, rule_id = rule_id.strip().split('|', 1) message = '[' + custom_rule_name + '] ' + message # This is to add support for warnings, style-checks, ... that can have # the rule id as [rule_id:rule_switch] # (i.e., something like # f.adb:10:07: variable "uninitialized" is read but never assigned # [warnings:v]) rule_switch = '' if ':' in rule_id: rule_id, rule_switch = rule_id.strip().split(':', 1) message = '[' + rule_id + ':' + rule_switch + '] ' + message # Build message including instance information new_msg = message + ' [' + instance + ']' self.__add_message(src, line, column, rule_id, new_msg) def __add_message(self, src, line, column, rule_id, msg, tag=None): """Add GNATcheck message to current session database. :param str src: Message source file. :param str line: Message line number. :param str column: Message column number. :param str rule_id: Message's rule identifier. :param str msg: Description of the message. """ # Cache the rules if rule_id in self.rules: rule = self.rules[rule_id] else: rule = GNAThub.Rule(rule_id, rule_id, GNAThub.RULE_KIND, self.tool) self.rules[rule_id] = rule # Set predefined info ranking for all GNATcheck messages ranking = GNAThub.RANKING_INFO # Cache the messages if (rule, msg, ranking) in self.messages: message = self.messages[(rule, msg, ranking)] else: if tag: message = GNAThub.Message(rule, msg, ranking, 0, tag) else: message = GNAThub.Message(rule, msg, ranking) self.messages[(rule, msg, ranking)] = message # Add the message to the given resource self.bulk_data[src].append( [message, int(line), int(column), int(column)]) def __do_bulk_insert(self): """Insert the gnatcheck messages in bulk on each resource.""" # List of resource messages suitable for tool level bulk insertion resources_messages = [] for src in self.bulk_data: base = GNAThub.Project.source_file(os.path.basename(src)) resource = GNAThub.Resource.get(base) if resource: resources_messages.append([resource, self.bulk_data[src]]) self.tool.add_messages(resources_messages, [])