dcerpc-nt: add UNION_ALIGN_TO... helpers
[wireshark-sm.git] / tools / check_static.py
blob773c0d6076afc5522ec3de423e43f6bb90b62ae8
1 #!/usr/bin/env python3
2 # Wireshark - Network traffic analyzer
3 # By Gerald Combs <gerald@wireshark.org>
4 # Copyright 1998 Gerald Combs
6 # SPDX-License-Identifier: GPL-2.0-or-later
8 import os
9 import re
10 import subprocess
11 import argparse
12 import signal
14 # Look for dissector symbols that could/should be static.
15 # This will not run on Windows, unless/until we check the platform
16 # and use (I think) dumpbin.exe
18 # N.B. Will report false positives if symbols are extern'd rather than
19 # declared in a header file.
21 # Try to exit soon after Ctrl-C is pressed.
22 should_exit = False
24 def signal_handler(sig, frame):
25 global should_exit
26 should_exit = True
27 print('You pressed Ctrl+C - exiting')
29 signal.signal(signal.SIGINT, signal_handler)
31 # Allow this as a default build folder name...
32 build_folder = os.getcwd() + '-build'
35 # Record which symbols are referred to (by a set of files).
36 class CalledSymbols:
37 def __init__(self):
38 self.referred = set()
40 def addCalls(self, file):
41 if should_exit:
42 exit(1)
44 # Make sure that file is built.
45 last_dir = os.path.split(os.path.dirname(file))[-1]
46 if file.find('ui/cli') != -1:
47 # A tshark target-only file
48 object_file = os.path.join(build_folder, 'CMakeFiles', ('tshark' + '.dir'), file + '.o')
49 elif file.find('ui/qt') != -1:
50 object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', ('qtui' + '.dir'), os.path.basename(file) + '.o')
51 else:
52 if file.endswith('dissectors.c'):
53 object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', 'dissector-registration' + '.dir', os.path.basename(file) + '.o')
54 else:
55 object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', last_dir + '.dir', os.path.basename(file) + '.o')
56 if not os.path.exists(object_file):
57 # Not built for whatever reason..
58 #print('Warning -', object_file, 'does not exist')
59 return
61 # Run command to check symbols.
62 command = ['nm', object_file]
63 for f in subprocess.check_output(command).splitlines():
64 line = str(f)[2:-1]
65 # Lines might, or might not, have an address before letter and symbol.
66 p1 = re.compile(r'[0-9a-f]* ([a-zA-Z]) (.*)')
67 p2 = re.compile(r'[ ]* ([a-zA-Z]) (.*)')
69 m = p1.match(line)
70 if not m:
71 m = p2.match(line)
72 if m:
73 letter = m.group(1)
74 function_name = m.group(2)
76 # Only interested in undefined/external references to symbols.
77 if letter == 'U':
78 self.referred.add(function_name)
82 # Record which symbols are defined in a single dissector file.
83 class DefinedSymbols:
84 def __init__(self, file):
85 self.filename = file
86 self.global_symbols = {} # map from defined symbol -> whole output-line
87 self.header_file_contents = None
88 self.from_generated_file = isGeneratedFile(file)
90 # Make sure that file is built.
91 if self.filename.startswith('epan'):
92 object_file = os.path.join(build_folder, 'epan', 'dissectors', 'CMakeFiles', 'dissectors.dir', os.path.basename(file) + '.o')
93 elif self.filename.startswith('plugins'):
94 plugin_base_dir = os.path.dirname(file)
95 plugin_base_name = os.path.basename(plugin_base_dir)
96 object_file = os.path.join(build_folder, plugin_base_dir, 'CMakeFiles', plugin_base_name + '.dir', os.path.basename(file) + '.o')
97 else:
98 #print("Warning - can't determine object file for ", self.filename)
99 return
100 if not os.path.exists(object_file):
101 #print('Warning -', object_file, 'does not exist')
102 return
104 # Get header file contents if available
105 header_file= file.replace('.c', '.h')
106 try:
107 f = open(header_file, 'r')
108 self.header_file_contents = f.read()
109 except IOError:
110 pass
112 # Run command to see which symbols are defined
113 command = ['nm', object_file]
114 for f in subprocess.check_output(command).splitlines():
115 # Line consists of whitespace, [address], letter, symbolName
116 line = str(f)[2:-1]
117 p = re.compile(r'[0-9a-f]* ([a-zA-Z]) (.*)')
118 m = p.match(line)
119 if m:
120 letter = m.group(1)
121 function_name = m.group(2)
122 # Globally-defined symbols. Would be 't' or 'd' if already static..
123 if letter in 'TD':
124 self.addDefinedSymbol(function_name, line)
126 def addDefinedSymbol(self, symbol, line):
127 self.global_symbols[symbol] = line
129 # Check if a given symbol is mentioned in headers
130 def mentionedInHeaders(self, symbol):
131 if self.header_file_contents:
132 if self.header_file_contents.find(symbol) != -1:
133 return True
134 # Also check some of the 'common' header files that don't match the dissector file name.
135 # TODO: could cache the contents of these files?
136 common_mismatched_headers = [ os.path.join('epan', 'dissectors', 'packet-ncp-int.h'),
137 os.path.join('epan', 'dissectors', 'packet-mq.h'),
138 os.path.join('epan', 'dissectors', 'packet-ip.h'),
139 os.path.join('epan', 'dissectors', 'packet-gsm_a_common.h'),
140 os.path.join('epan', 'dissectors', 'packet-epl.h'),
141 os.path.join('epan', 'dissectors', 'packet-bluetooth.h'),
142 os.path.join('epan', 'dissectors', 'packet-dcerpc.h'),
143 os.path.join('epan', 'ip_opts.h'),
144 os.path.join('epan', 'eap.h')]
145 for hf in common_mismatched_headers:
146 try:
147 f = open(hf)
148 contents = f.read()
149 if contents.find(symbol) != -1:
150 return True
151 except EnvironmentError:
152 pass
154 return False
156 def checkIfSymbolsAreCalled(self, called_symbols):
157 global issues_found
158 for f in self.global_symbols:
159 if f not in called_symbols:
160 mentioned_in_header = self.mentionedInHeaders(f)
161 fun = self.global_symbols[f]
162 print(self.filename, '' if not self.from_generated_file else '(GENERATED)',
163 '(' + fun + ')',
164 'is not referred to so could be static?', '(declared in header)' if mentioned_in_header else '')
165 issues_found += 1
169 # Helper functions.
171 def isDissectorFile(filename):
172 # Ignoring usb.c & errno.c
173 p = re.compile(r'(packet|file)-.*\.c')
174 return p.match(filename)
176 # Test for whether the given dissector file was automatically generated.
177 def isGeneratedFile(filename):
178 # Check file exists - e.g. may have been deleted in a recent commit.
179 if not os.path.exists(filename):
180 return False
182 if not filename.endswith('.c'):
183 return False
185 # Open file
186 f_read = open(os.path.join(filename), 'r')
187 lines_tested = 0
188 for line in f_read:
189 # The comment to say that its generated is near the top, so give up once
190 # get a few lines down.
191 if lines_tested > 10:
192 f_read.close()
193 return False
194 if (line.find('Generated automatically') != -1 or
195 line.find('Autogenerated from') != -1 or
196 line.find('is autogenerated') != -1 or
197 line.find('automatically generated by Pidl') != -1 or
198 line.find('Created by: The Qt Meta Object Compiler') != -1 or
199 line.find('This file was generated') != -1 or
200 line.find('This filter was automatically generated') != -1):
202 f_read.close()
203 return True
204 lines_tested = lines_tested + 1
206 # OK, looks like a hand-written file!
207 f_read.close()
208 return False
211 def findDissectorFilesInFolder(folder, include_generated):
212 # Look at files in sorted order, to give some idea of how far through is.
213 tmp_files = []
215 for f in sorted(os.listdir(folder)):
216 if should_exit:
217 return
218 if isDissectorFile(f):
219 if include_generated or not isGeneratedFile(os.path.join('epan', 'dissectors', f)):
220 filename = os.path.join(folder, f)
221 tmp_files.append(filename)
222 return tmp_files
224 def findFilesInFolder(folder):
225 # Look at files in sorted order, to give some idea of how far through is.
226 tmp_files = []
228 for f in sorted(os.listdir(folder)):
229 if should_exit:
230 return
231 if f.endswith('.c') or f.endswith('.cpp'):
232 filename = os.path.join(folder, f)
233 tmp_files.append(filename)
234 return tmp_files
237 def is_dissector_file(filename):
238 p = re.compile(r'.*(packet|file)-.*\.c')
239 return p.match(filename)
244 #################################################################
245 # Main logic.
247 # command-line args. Controls which dissector files should be checked.
248 # If no args given, will just scan epan/dissectors folder.
249 parser = argparse.ArgumentParser(description='Check calls in dissectors')
250 parser.add_argument('--build-folder', action='store', default='',
251 help='build folder', required=False)
252 parser.add_argument('--file', action='append',
253 help='specify individual dissector file to test')
254 parser.add_argument('--commits', action='store',
255 help='last N commits to check')
256 parser.add_argument('--open', action='store_true',
257 help='check open files')
259 args = parser.parse_args()
261 issues_found = 0
263 # Get files from wherever command-line args indicate.
264 files = []
266 if args.build_folder:
267 build_folder = args.build_folder
269 if args.file:
270 # Add specified file(s)
271 for f in args.file:
272 if not os.path.isfile(f) and not f.startswith('epan'):
273 f = os.path.join('epan', 'dissectors', f)
274 if not os.path.isfile(f):
275 print('Chosen file', f, 'does not exist.')
276 exit(1)
277 else:
278 files.append(f)
279 elif args.commits:
280 # Get files affected by specified number of commits.
281 command = ['git', 'diff', '--name-only', 'HEAD~' + args.commits]
282 files = [f.decode('utf-8')
283 for f in subprocess.check_output(command).splitlines()]
284 # Will examine dissector files only
285 files = list(filter(lambda f : is_dissector_file(f), files))
286 elif args.open:
287 # Unstaged changes.
288 command = ['git', 'diff', '--name-only']
289 files = [f.decode('utf-8')
290 for f in subprocess.check_output(command).splitlines()]
291 # Only interested in dissector files.
292 files = list(filter(lambda f : is_dissector_file(f), files))
293 # Staged changes.
294 command = ['git', 'diff', '--staged', '--name-only']
295 files_staged = [f.decode('utf-8')
296 for f in subprocess.check_output(command).splitlines()]
297 # Only interested in dissector files.
298 files_staged = list(filter(lambda f : is_dissector_file(f), files_staged))
299 for f in files:
300 files.append(f)
301 for f in files_staged:
302 if f not in files:
303 files.append(f)
304 else:
305 # Find all dissector files from folder.
306 files = findDissectorFilesInFolder(os.path.join('epan', 'dissectors'),
307 include_generated=True)
310 # If scanning a subset of files, list them here.
311 print('Examining:')
312 if args.file or args.commits or args.open:
313 if files:
314 print(' '.join(files), '\n')
315 else:
316 print('No files to check.\n')
317 else:
318 print('All dissector modules\n')
321 if not os.path.isdir(build_folder):
322 print('Build directory not valid', build_folder, '- please set with --build-folder')
323 exit(1)
326 # Get the set of called functions and referred-to data.
327 called = CalledSymbols()
328 for d in findDissectorFilesInFolder(os.path.join('epan', 'dissectors'), include_generated=True):
329 called.addCalls(d)
330 called.addCalls(os.path.join('epan', 'dissectors', 'dissectors.c'))
331 # Also check calls from GUI code
332 for d in findFilesInFolder('ui'):
333 called.addCalls(d)
334 for d in findFilesInFolder(os.path.join('ui', 'qt')):
335 called.addCalls(d)
336 # These are from tshark..
337 for d in findFilesInFolder(os.path.join('ui', 'cli')):
338 called.addCalls(d)
341 # Now check identified dissector files.
342 for f in files:
343 if should_exit:
344 exit(1)
345 # Are these symbols called - or could they be deleted or static????
346 DefinedSymbols(f).checkIfSymbolsAreCalled(called.referred)
348 # Show summary.
349 print(issues_found, 'issues found')