# 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 GNATstack command-line tool.
It exports the GNATstack 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 os
import GNAThub
from GNAThub import Plugin, Reporter, Runner
from xml.etree import ElementTree
from xml.etree.ElementTree import ParseError
[docs]class GNATstack(Plugin, Runner, Reporter):
"""GNATstack plugin for GNAThub."""
# GNATstack exits with an error code of 1 even on a successful run
VALID_EXIT_CODES = (0, 1)
RANKING = GNAThub.RANKING_INFO
[docs] def __init__(self):
super(GNATstack, self).__init__()
if GNAThub.dry_run_without_project():
return
self.tool = None
# FIXME: For now we can't control where the xml file is created:
# it is always created in the project directory
self.output = os.path.join(os.path.dirname(GNAThub.Project.path()),
"stack_usage.xml")
self.resources = {}
# Map of ID => entity
self.subprograms = {}
# Map of ID => name
self.subprograms_without_location = {}
def __cmd_line(self):
"""Create GNATstack command line arguments list.
:return: the GNATstack command line
:rtype: collections.Iterable[str]
"""
cmd_line = [
'gnatstack', '-Q', '-x', '-Wa',
'-P', GNAThub.Project.path()] + GNAThub.Project.scenario_switches()
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 GNATstack.
Returns according to the success of the execution of the tool:
* ``GNAThub.EXEC_SUCCESS``: on successful execution
* ``GNAThub.EXEC_FAILURE``: on any error
"""
return GNAThub.EXEC_SUCCESS if GNAThub.Run(
self.name, self.__cmd_line()
).status in GNATstack.VALID_EXIT_CODES else GNAThub.EXEC_FAILURE
[docs] def pp_name(self, name):
if '<' in name:
return name
else:
return name.split('.')[-1].title()
[docs] def pp_msg(self, id, msg):
return "[{}] {}".format(msg, self.subprograms[id].name)
[docs] def report(self):
"""Parse GNATstack 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
"""
# Clear existing references only if not incremental run
if not GNAThub.incremental():
self.log.info('clear existing results if any')
GNAThub.Tool.clear_references(self.name)
self.info('analyse report')
self.tool = GNAThub.Tool(self.name)
if not os.path.exists(self.output):
self.error('no report found')
return GNAThub.EXEC_FAILURE
self.log.debug('parse XML report: %s', self.output)
# List of resource messages suitable for tool level bulk insertion
resources_messages = []
# Map of list of messages by entities
entities_messages_map = {}
# List of entity messages suitable for tool level bulk insertion
entities_messages = []
# List of indirect call location
indirect_loc_list = []
try:
tree = ElementTree.parse(self.output)
global_node = tree.find('./global')
# Retrieve the metrics and the map of subprogram by id
subprograms = tree.find('./subprogramset').findall('./subprogram')
if subprograms:
unknown_global = GNAThub.Rule("Unknown Global Stack Usage",
"Unknown Global Stack Usage",
GNAThub.METRIC_KIND,
self.tool)
static_global = GNAThub.Rule("Static Global Stack Usage",
"Static Global Stack Usage",
GNAThub.METRIC_KIND,
self.tool)
unknown_local = GNAThub.Rule("Unknown Local Stack Usage",
"Unknown Local Stack Usage",
GNAThub.METRIC_KIND,
self.tool)
static_local = GNAThub.Rule("Static Local Stack Usage",
"Static Local Stack Usage",
GNAThub.METRIC_KIND,
self.tool)
for node in subprograms:
subprogram_id = node.attrib.get('id')
locations = node.find('./locationset').findall('./location')
global_usage = node.find('./globalstackusage')
local_usage = node.find('./localstackusage')
name = node.attrib.get('prefixname')
if name == "indirect call":
# The columns are only defined here so save them for later
line = locations[0].attrib.get('line')
column = locations[0].attrib.get('column')
indirect_loc_list.append([line, column])
continue
else:
name = self.pp_name(name)
if not locations:
if subprogram_id not in self.subprograms_without_location:
self.subprograms_without_location[subprogram_id] = name
for loc in locations:
file = loc.attrib.get('file')
line = loc.attrib.get('line')
column = loc.attrib.get('column')
if file in self.resources:
resource = self.resources[file]
else:
resource = GNAThub.Resource(file, GNAThub.FILE_KIND)
self.resources[file] = resource
# entities default value for kind is set to "procedure"
entity = GNAThub.Entity(name, "action",
int(line), int(column),
int(column), resource)
# Only link the id to the first location of the entity
if subprogram_id not in self.subprograms:
self.subprograms[subprogram_id] = entity
else:
continue
size = global_usage.attrib.get('size')
if global_usage.attrib.get('qualifier') == "UNKNOWN":
metric = unknown_global
else:
metric = static_global
global_metric = GNAThub.Message(metric,
size,
ranking=GNATstack.RANKING)
size = local_usage.attrib.get('size')
if local_usage.attrib.get('qualifier') == "UNKNOWN":
metric = unknown_local
else:
metric = static_local
local_metric = GNAThub.Message(metric,
size,
ranking=GNATstack.RANKING)
entities_messages_map[subprogram_id] = (
[[global_metric, 0, 1, 1], [local_metric, 0, 1, 1]])
# Analyse the indirect calls
indirects = global_node.find('./indirectset').findall('./indirect')
if indirects:
indirect_rule = GNAThub.Rule("Indirect Call",
"Indirect Call",
GNAThub.RULE_KIND,
self.tool)
for node in indirects:
indirect_id = node.attrib.get('id')
if indirect_id not in self.subprograms:
continue
set = node.find('./indirectcallset').findall('./indirectcall')
for call in set:
line = call.find('./line').find('./value').text
# Go through the list of saved locations and use the
# line to retrieve a corresponding column
column = 1
pos = -1
for ix in range(len(indirect_loc_list)):
if indirect_loc_list[ix][0] == line:
pos = ix
column = indirect_loc_list[pos][1]
continue
if pos != -1:
indirect_loc_list.pop(pos)
message = GNAThub.Message(indirect_rule,
self.pp_msg(indirect_id,
"indirect call"),
ranking=GNATstack.RANKING)
entities_messages_map[indirect_id].append([message,
int(line),
int(column),
int(column)])
# Analyse the external calls
externals = global_node.find('./externalset').findall('./external')
if externals:
external_rule = GNAThub.Rule("External Call",
"External Call",
GNAThub.RULE_KIND,
self.tool)
for node in externals:
subprogram_id = node.attrib.get('id')
if subprogram_id not in self.subprograms:
continue
message = GNAThub.Message(external_rule,
self.pp_msg(subprogram_id,
"external call"),
ranking=GNATstack.RANKING)
entities_messages_map[subprogram_id].append([message, 0, 1, 1])
# Analyse the potential cycle
cycles = global_node.find('./cycleset').findall('./cycle')
if cycles:
cycle_rule = GNAThub.Rule("Potential Cycle",
"Potential Cycle",
GNAThub.RULE_KIND,
self.tool)
for node in cycles:
cycle_subprograms = node.findall('./subprogram')
cycle_list = []
for sub in cycle_subprograms:
subprogram_id = sub.attrib.get('id')
cycle_list.append(self.subprograms[subprogram_id].name)
subprogram_id = cycle_subprograms[0].attrib.get('id')
cycle_list.append(self.subprograms[subprogram_id].name)
message = GNAThub.Message(cycle_rule,
"potential cycle detected:\n\t\t" +
"\n\t\t".join(cycle_list),
ranking=GNATstack.RANKING)
entities_messages_map[subprogram_id].append([message, 0, 1, 1])
# Analyse the unbounded frames
unboundeds = (
global_node.find('./unboundedset').findall('./unbounded'))
if unboundeds:
unbounded_rule = GNAThub.Rule("Unbounded Frame",
"Unbounded Frame",
GNAThub.RULE_KIND,
self.tool)
for node in unboundeds:
subprogram_id = node.attrib.get('id')
if subprogram_id in self.subprograms:
message = GNAThub.Message(unbounded_rule,
self.pp_msg(subprogram_id,
"unbounded frame"),
ranking=GNATstack.RANKING)
entities_messages_map[subprogram_id].append([message,
0, 1, 1])
# Analyse the entry points
entries = tree.find('./entryset').findall('./entry')
# There is always an entry, so create the rule anyway
entry_rule = GNAThub.Rule("Entry point",
"Entry point",
GNAThub.RULE_KIND,
self.tool)
for node in entries:
subprogram_id = node.attrib.get('id')
if subprogram_id not in self.subprograms:
continue
entity = self.subprograms[subprogram_id]
local_stack = node.find('./localstackusage')
size = local_stack.attrib.get('size')
qualifier = local_stack.attrib.get('qualifier')
if qualifier == "UNKNOWN":
text = "The estimated"
else:
text = "The"
text += (' call stack size for the entry point "%s" is %s' %
(entity.name, str(size)))
callchain_list = []
for sub in node.find('./callchain').findall('./subprogram'):
chain_id = sub.attrib.get('id')
if chain_id in self.subprograms:
callchain_list.append(self.subprograms[chain_id].name)
elif chain_id in self.subprograms_without_location:
callchain_list.append(
self.subprograms_without_location[chain_id])
else:
continue
text += (" and the callchain is:\n\t\t%s" %
"\n\t\t".join(callchain_list))
message = GNAThub.Message(entry_rule,
text,
ranking=GNATstack.RANKING)
entities_messages_map[subprogram_id].append([message, 0, 1, 1])
# Project message explaining the accuracy of the metrics
accurate = global_node.find('./accurate')
if accurate.find('./value').text == "FALSE":
project = GNAThub.Resource(GNAThub.Project.name(),
GNAThub.PROJECT_KIND)
rule = GNAThub.Rule("Accuracy", "Accuracy",
GNAThub.RULE_KIND, self.tool)
text = ("worst case may not be accurate because of: " +
("indirect calls/" if indirects else "") +
("cycles/" if cycles else "") +
("unbounded frames/" if unboundeds else "") +
("external calls" if externals else ""))
text = text[:-1] if text[-1] == '/' else text
message = GNAThub.Message(rule,
text,
ranking=GNATstack.RANKING)
resources_messages.append([project, [[message, 0, 1, 1]]])
# Insert the messages and the metrics
for key, value in entities_messages_map.items():
entities_messages.append([self.subprograms[key], value])
self.tool.add_messages(resources_messages, entities_messages)
except ParseError as why:
self.log.exception('failed to parse XML report')
self.error('%s (%s:%s)' % (why, why.filename, why.lineno))
return GNAThub.EXEC_FAILURE
else:
return GNAThub.EXEC_SUCCESS