blob: 963a313e84dea77fcdb0a690d442a32dedf6aab3 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
This module can fetch elements (include directives, functions, etc) from C source codes.
The main purpose of this module is help unit testing by isolating functions from the
rest of the code.
"""
import enum
import os
import sys
# pylint exceptions are used because of the necessary Config.set_library_path call.
from clang.cindex import Config
if "CLANG_LIBRARY_PATH" in os.environ:
Config.set_library_path(os.environ["CLANG_LIBRARY_PATH"])
import clang.cindex # pylint: disable=wrong-import-position
from c_picker.coverage import MappingDescriptor # pylint: disable=wrong-import-position
class CPicker:
""" CPicker can fetch C source element from a file matching the parameters and options. """
class Type(enum.Enum):
""" C source element type """
include = clang.cindex.CursorKind.INCLUSION_DIRECTIVE
function = clang.cindex.CursorKind.FUNCTION_DECL
variable = clang.cindex.CursorKind.VAR_DECL
class Option(enum.Enum):
""" Parameter for modifying the behaviour of the fetcher """
class RemoveStaticProcessor:
""" Removes 'static' from function declaration """
@staticmethod
def is_matching(node):
""" Checks if the storage class STATIC """
return node.storage_class == clang.cindex.StorageClass.STATIC
@staticmethod
def process(lines):
""" Removes 'static' before the function body """
processed_lines = []
function_body_started = False
for line in lines:
if not function_body_started:
processed_lines.append(line.replace("static ", ""))
function_body_started = "{" in line
else:
processed_lines.append(line)
return processed_lines
remove_static = RemoveStaticProcessor
class ElementDescriptor:
""" Data structure of matching parameters """
def __init__(self, file_name, element_type, element_name=None):
self.file_name = file_name
self.element_type = element_type
self.element_name = element_name
self.args = None
self.options = None
def set_args(self, args):
""" Setting arguments of the parser """
self.args = args
def set_options(self, options):
""" Setting options for the fetcher """
self.options = options
def is_single_matching_type(self):
""" There's only a single matching element """
return bool(self.element_name)
def is_matching(self, node):
""" The node is matching the element defined in this instance """
if self.element_type == CPicker.Type.include:
return ((node.kind == self.element_type.value) and
(str(node.location.file) == self.file_name))
if self.element_type == CPicker.Type.function:
return ((node.kind == self.element_type.value) and
(str(node.location.file) == self.file_name) and
(node.spelling == self.element_name) and
node.is_definition())
if self.element_type == CPicker.Type.variable:
return ((node.kind == self.element_type.value) and
(str(node.location.file) == self.file_name) and
(node.spelling == self.element_name))
raise Exception("Invalid element type")
def get_option_processors(self, node):
""" Get processor function for matching options """
processors = []
for option in self.options:
if option.value.is_matching(node):
processors.append(option.value)
return processors
def __init__(self, output=None, print_dependencies=False):
self.output = output if output else sys.stdout
self.print_dependencies = print_dependencies
def generate_header_comment(self):
""" Warning comment in the first line of the code """
self.output.write("/* DO NOT MODIFY! Generated by c-picker. */\n")
def generate_dependencies(self, elements):
""" Print the set of files included in the elements list """
dependencies = set()
for element in elements:
dependencies.add(os.path.abspath(element.file_name))
self.output.write(";".join(dependencies))
def fetch(self, element_descriptor):
""" Fetching C source element from the file """
# Parsing file with clang
try:
parser_options = clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
index = clang.cindex.Index.create()
translation_unit = index.parse(element_descriptor.file_name,
args=element_descriptor.args,
options=parser_options)
except clang.cindex.TranslationUnitLoadError as exception:
raise Exception("Failed to parse " + element_descriptor.file_name + ": "
+ str(exception))
# Element comment
if element_descriptor.element_name:
element_text = element_descriptor.element_name
else:
element_text = element_descriptor.element_type.name
self.output.write("\n/* %s from %s */\n" % (element_text, element_descriptor.file_name))
# Searching for matching elements
for node in translation_unit.cursor.walk_preorder():
if element_descriptor.is_matching(node):
self.dump(node, element_descriptor.get_option_processors(node))
if element_descriptor.is_single_matching_type():
break
else:
if element_descriptor.is_single_matching_type():
raise Exception("%s not found in %s"
% (element_descriptor.element_name, element_descriptor.file_name))
def dump(self, node, options_processors):
""" Dump the contents of a node to the specified output """
with open(str(node.location.file), "r") as source_file:
source_lines = source_file.readlines()
source_lines = source_lines[node.extent.start.line - 1 : node.extent.end.line]
if len(source_lines) == 1:
source_lines = [source_lines[0][node.extent.start.column - 1 :
node.extent.end.column - 1]]
elif len(source_lines) > 1:
source_lines = ([source_lines[0][node.extent.start.column - 1 :]] +
source_lines[1:-1] +
[source_lines[-1][: node.extent.end.column - 1]])
for processor in options_processors:
source_lines = processor.process(source_lines)
# Mapping information of the original source
mapping_descriptor = MappingDescriptor(str(node.location.file), node.extent.start.line)
self.output.write(MappingDescriptor.serialize(mapping_descriptor) + "\n")
for source_line in source_lines:
self.output.write(source_line)
if node.kind == CPicker.Type.variable.value:
self.output.write(";")
self.output.write("\n")
def process(self, elements):
""" Processes element list """
try:
if not self.print_dependencies:
self.generate_header_comment()
for element in elements:
self.fetch(element)
else:
self.generate_dependencies(elements)
except clang.cindex.LibclangError as _:
raise Exception("Please ensure you have the correct version of libclang installed" +
" and the CLANG_LIBRARY_PATH environment variable set.")