blob: 963a313e84dea77fcdb0a690d442a32dedf6aab3 [file] [log] [blame]
Imre Kis124f0e32021-02-12 18:03:24 +01001#!/usr/bin/env python3
2# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""
7This module can fetch elements (include directives, functions, etc) from C source codes.
8The main purpose of this module is help unit testing by isolating functions from the
9rest of the code.
10"""
11
12import enum
13import os
14import sys
15
16# pylint exceptions are used because of the necessary Config.set_library_path call.
17from clang.cindex import Config
18if "CLANG_LIBRARY_PATH" in os.environ:
19 Config.set_library_path(os.environ["CLANG_LIBRARY_PATH"])
20import clang.cindex # pylint: disable=wrong-import-position
21
22from c_picker.coverage import MappingDescriptor # pylint: disable=wrong-import-position
23
24class CPicker:
25 """ CPicker can fetch C source element from a file matching the parameters and options. """
26
27 class Type(enum.Enum):
28 """ C source element type """
29 include = clang.cindex.CursorKind.INCLUSION_DIRECTIVE
30 function = clang.cindex.CursorKind.FUNCTION_DECL
31 variable = clang.cindex.CursorKind.VAR_DECL
32
33 class Option(enum.Enum):
34 """ Parameter for modifying the behaviour of the fetcher """
35
36 class RemoveStaticProcessor:
37 """ Removes 'static' from function declaration """
38
39 @staticmethod
40 def is_matching(node):
41 """ Checks if the storage class STATIC """
42 return node.storage_class == clang.cindex.StorageClass.STATIC
43
44 @staticmethod
45 def process(lines):
46 """ Removes 'static' before the function body """
47 processed_lines = []
48 function_body_started = False
49 for line in lines:
50 if not function_body_started:
51 processed_lines.append(line.replace("static ", ""))
52 function_body_started = "{" in line
53 else:
54 processed_lines.append(line)
55 return processed_lines
56
57 remove_static = RemoveStaticProcessor
58
59 class ElementDescriptor:
60 """ Data structure of matching parameters """
61 def __init__(self, file_name, element_type, element_name=None):
62 self.file_name = file_name
63 self.element_type = element_type
64 self.element_name = element_name
65 self.args = None
66 self.options = None
67
68 def set_args(self, args):
69 """ Setting arguments of the parser """
70 self.args = args
71
72 def set_options(self, options):
73 """ Setting options for the fetcher """
74 self.options = options
75
76 def is_single_matching_type(self):
77 """ There's only a single matching element """
78 return bool(self.element_name)
79
80 def is_matching(self, node):
81 """ The node is matching the element defined in this instance """
82 if self.element_type == CPicker.Type.include:
83 return ((node.kind == self.element_type.value) and
84 (str(node.location.file) == self.file_name))
85
86 if self.element_type == CPicker.Type.function:
87 return ((node.kind == self.element_type.value) and
88 (str(node.location.file) == self.file_name) and
89 (node.spelling == self.element_name) and
90 node.is_definition())
91 if self.element_type == CPicker.Type.variable:
92 return ((node.kind == self.element_type.value) and
93 (str(node.location.file) == self.file_name) and
94 (node.spelling == self.element_name))
95 raise Exception("Invalid element type")
96
97 def get_option_processors(self, node):
98 """ Get processor function for matching options """
99 processors = []
100 for option in self.options:
101 if option.value.is_matching(node):
102 processors.append(option.value)
103 return processors
104
105 def __init__(self, output=None, print_dependencies=False):
106 self.output = output if output else sys.stdout
107 self.print_dependencies = print_dependencies
108
109 def generate_header_comment(self):
110 """ Warning comment in the first line of the code """
111 self.output.write("/* DO NOT MODIFY! Generated by c-picker. */\n")
112
113 def generate_dependencies(self, elements):
114 """ Print the set of files included in the elements list """
115 dependencies = set()
116
117 for element in elements:
118 dependencies.add(os.path.abspath(element.file_name))
119
120 self.output.write(";".join(dependencies))
121
122 def fetch(self, element_descriptor):
123 """ Fetching C source element from the file """
124
125 # Parsing file with clang
126 try:
127 parser_options = clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
128 index = clang.cindex.Index.create()
129 translation_unit = index.parse(element_descriptor.file_name,
130 args=element_descriptor.args,
131 options=parser_options)
132 except clang.cindex.TranslationUnitLoadError as exception:
133 raise Exception("Failed to parse " + element_descriptor.file_name + ": "
134 + str(exception))
135
136 # Element comment
137 if element_descriptor.element_name:
138 element_text = element_descriptor.element_name
139 else:
140 element_text = element_descriptor.element_type.name
141
142 self.output.write("\n/* %s from %s */\n" % (element_text, element_descriptor.file_name))
143
144 # Searching for matching elements
145 for node in translation_unit.cursor.walk_preorder():
146 if element_descriptor.is_matching(node):
147 self.dump(node, element_descriptor.get_option_processors(node))
148 if element_descriptor.is_single_matching_type():
149 break
150 else:
151 if element_descriptor.is_single_matching_type():
152 raise Exception("%s not found in %s"
153 % (element_descriptor.element_name, element_descriptor.file_name))
154
155 def dump(self, node, options_processors):
156 """ Dump the contents of a node to the specified output """
157 with open(str(node.location.file), "r") as source_file:
158 source_lines = source_file.readlines()
159 source_lines = source_lines[node.extent.start.line - 1 : node.extent.end.line]
160
161 if len(source_lines) == 1:
162 source_lines = [source_lines[0][node.extent.start.column - 1 :
163 node.extent.end.column - 1]]
164 elif len(source_lines) > 1:
165 source_lines = ([source_lines[0][node.extent.start.column - 1 :]] +
166 source_lines[1:-1] +
167 [source_lines[-1][: node.extent.end.column - 1]])
168
169 for processor in options_processors:
170 source_lines = processor.process(source_lines)
171
172 # Mapping information of the original source
173 mapping_descriptor = MappingDescriptor(str(node.location.file), node.extent.start.line)
174 self.output.write(MappingDescriptor.serialize(mapping_descriptor) + "\n")
175
176 for source_line in source_lines:
177 self.output.write(source_line)
178 if node.kind == CPicker.Type.variable.value:
179 self.output.write(";")
180 self.output.write("\n")
181
182 def process(self, elements):
183 """ Processes element list """
184 try:
185 if not self.print_dependencies:
186 self.generate_header_comment()
187
188 for element in elements:
189 self.fetch(element)
190 else:
191 self.generate_dependencies(elements)
192 except clang.cindex.LibclangError as _:
193 raise Exception("Please ensure you have the correct version of libclang installed" +
194 " and the CLANG_LIBRARY_PATH environment variable set.")