# 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, [])