fw4spl
codingstyle.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 
4 """
5 Make sure you respect the minimal coding rules and gently reformat files for you.
6 
7 .gitconfig configuration :
8 
9 [fw4spl-hooks]
10  hooks = codingstyle
11 
12 [codingstyle-hook]
13  source-patterns = *.cpp *.cxx *.c
14  header-patterns = *.hpp *.hxx *.h
15  misc-patterns = *.cmake *.txt *.xml *.json
16  uncrustify-path=C:\Program files\uncrustify\uncrustify.exe
17  additional-projects = "D:/Dev/src/fw4spl-ar;D:/Dev/src/fw4spl-ext"
18 
19 Available options are :
20 source-patterns : file patterns to process as source code files - default to *.cpp *.cxx *.c
21 header-patterns : file patterns to process as header files - default to *.hpp *.hxx *.h
22 misc-patterns : file patterns to process as non-source code files (build, configuration, etc...)
23  Reformatting is limited to TABs and EOL - default to *.options *.cmake *.txt *.xml
24 uncrustify-path : path to the uncrustify program - default to uncrustify
25 additional-projects : additional fw4spl repositories paths used to sort includes (separated with a ;).
26  default parent folder of the current repository.
27 
28 """
29 
30 import os
31 import re
32 from fnmatch import fnmatch
33 
34 import common
35 import sortincludes
36 from common import FormatReturn
37 
38 SEPARATOR = '%s\n' % ('-' * 79)
39 FILEWARN = lambda x: (' - %s') % os.path.relpath(x, common.get_repo_root())
40 UNCRUSTIFY_PATH = 'uncrustify'
41 BACKUP_LIST_FILE = 'backupList'
42 
43 LICENSE = '/\* \*\*\*\*\* BEGIN LICENSE BLOCK \*\*\*\*\*\n\
44  \* FW4SPL - Copyright \(C\) IRCAD, (.*).\n\
45  \* Distributed under the terms of the GNU Lesser General Public License \(LGPL\) as\n\
46  \* published by the Free Software Foundation.\n\
47  \* \*\*\*\*\*\* END LICENSE BLOCK \*\*\*\*\*\* \*/'
48 
49 
50 # ------------------------------------------------------------------------------
51 
52 def codingstyle(files, enable_reformat, check_lgpl, check_commits_date):
53  source_patterns = common.get_option('codingstyle-hook.source-patterns', default='*.cpp *.cxx *.c').split()
54  header_patterns = common.get_option('codingstyle-hook.header-patterns', default='*.hpp *.hxx *.h').split()
55  misc_patterns = common.get_option('codingstyle-hook.misc-patterns', default='*.cmake *.txt *.xml *.json').split()
56 
57  code_patterns = source_patterns + header_patterns
58  include_patterns = code_patterns + misc_patterns
59 
60  sort_includes = common.get_option('codingstyle-hook.sort-includes', default="true", type='--bool') == "true"
61 
62  global repoRoot
63  repoRoot = common.get_repo_root()
64 
65  if repoRoot is None:
66  common.warn("Cannot find 'fw4spl' repository structure")
67  parent_repo = ""
68  else:
69  parent_repo = os.path.abspath(os.path.join(repoRoot, os.pardir))
70 
71  fw4spl_configured_projects = common.get_option('codingstyle-hook.additional-projects', default=None)
72  fw4spl_projects = []
73 
74  if fw4spl_configured_projects is None:
75  # no additional-projects specified in config file. Default is parent repository folder
76  fw4spl_projects.append(parent_repo)
77  else:
78  fw4spl_projects = fw4spl_configured_projects.split(";")
79  # adds current repository folder to the additional-projects specified in config file.
80  fw4spl_projects.append(repoRoot)
81  # normalize pathname
82  fw4spl_projects = map(os.path.normpath, fw4spl_projects)
83  # remove duplicates
84  fw4spl_projects = list(set(fw4spl_projects))
85 
86  global UNCRUSTIFY_PATH
87 
88  if common.g_uncrustify_path_arg is not None and len(common.g_uncrustify_path_arg) > 0:
89  UNCRUSTIFY_PATH = common.g_uncrustify_path_arg
90  else:
91  UNCRUSTIFY_PATH = common.get_option('codingstyle-hook.uncrustify-path', default=UNCRUSTIFY_PATH,
92  type='--path').strip()
93 
94  common.note('Using uncrustify: ' + UNCRUSTIFY_PATH)
95 
96  if common.execute_command(UNCRUSTIFY_PATH + ' -v -q').status != 0:
97  common.error('Failed to launch uncrustify.\n')
98  return []
99 
100  checked = set()
101 
102  reformatted_list = []
104 
105  ret = False
106  count = 0
107  reformat_count = 0
108  for f in files:
109  if f in checked or not any(f.fnmatch(p) for p in include_patterns):
110  continue
111 
112  content = f.contents
113  if not common.binary(content):
114 
115  # Do this last because contents of the file will be modified by uncrustify
116  # Thus the variable content will no longer reflect the real content of the file
117  file_path = os.path.join(repoRoot, f.path)
118  if os.path.isfile(file_path):
119  res = format_file(file_path, enable_reformat, code_patterns, header_patterns, misc_patterns, check_lgpl,
120  sort_includes, f.status, check_commits_date)
121  count += 1
122  if res == FormatReturn.Modified:
123  reformatted_list.append(f.path)
124  reformat_count += 1
125  elif res == FormatReturn.Error:
126  # Error in reformatting
127  ret = True
128 
129  checked.add(f)
130 
131  common.note('%d file(s) checked, %d file(s) reformatted.' % (count, reformat_count))
132 
133  return ret, reformatted_list
134 
135 
136 # ------------------------------------------------------------------------------
137 
138 # Reformat file according to minimal coding-style rules
139 # Return True if anything as been modified along with a unified diff
140 def format_file(source_file, enable_reformat, code_patterns, header_patterns, misc_patterns, check_lgpl, sort_includes,
141  status, check_commits_date):
142  # Invoke uncrustify for source code files
143  if any(fnmatch(source_file, p) for p in code_patterns):
144 
145  common.trace('Launching uncrustify on : ' + source_file)
146  config_file = os.path.join(os.path.dirname(__file__), 'uncrustify.cfg')
147 
148  ret = FormatReturn()
149 
150  # Fix license year
151  if check_lgpl is True:
152  ret.add(fix_license_year(source_file, enable_reformat, status, check_commits_date))
153 
154  # Sort headers
155  if sort_includes is True:
156  ret.add(sortincludes.sort_includes(source_file, enable_reformat))
157 
158  if any(fnmatch(source_file, p) for p in header_patterns):
159  ret.add(fix_header_guard(source_file, enable_reformat))
160 
161  # Uncrustify
162  command = UNCRUSTIFY_PATH + ' -c ' + config_file + ' -q %s ' + source_file
163 
164  if enable_reformat is True:
165  # Check first
166  uncrustify = common.execute_command(command % '--check')
167 
168  if uncrustify.status != 0:
169  uncrustify = common.execute_command(command % '--replace --no-backup --if-changed')
170  if uncrustify.status != 0:
171  common.error('Uncrustify failure on file: ' + source_file)
172  common.error(uncrustify.out)
173  return FormatReturn.Error
174  ret.add(FormatReturn.Modified)
175  else:
176  uncrustify = common.execute_command(command % '--check')
177 
178  if uncrustify.status != 0:
179  common.error('Uncrustify failure on file: ' + source_file)
180  return FormatReturn.Error
181 
182  return ret.value
183 
184  # Replace only YEAR, TAB, CRLF and CR for miscellaneous files
185  elif any(fnmatch(source_file, p) for p in misc_patterns):
186 
187  common.trace('Parsing: ' + source_file + ' to replace CR, CRLF and TABs')
188 
189  str_old_file = open(source_file, 'rb').read()
190 
191  str_new_file = re.sub('\t', ' ', str_old_file)
192  tmp_str = re.sub('\r\n', '\n', str_new_file)
193  str_new_file = re.sub('\r', '\n', tmp_str)
194 
195  if str_old_file == str_new_file:
196  return FormatReturn.NotModified
197 
198  # Something has been changed, write the new file
199  open(source_file, 'wb').write(str_new_file)
200  return FormatReturn.Modified
201 
202 
203 # ------------------------------------------------------------------------------
204 
205 # Check licence header
206 def fix_license_year(path, enable_reformat, status, check_commits_date):
207  with open(path, 'r') as source_file:
208  content = source_file.read()
209 
210  common.trace('Checking for LGPL license in: ' + path)
211 
212  YEAR = common.get_file_datetime(path, check_commits_date).year
213 
214  # Look for the license pattern
215  licence_number = len(re.findall(LICENSE, content, re.MULTILINE))
216  if licence_number > 1:
217 
218  common.error("There should be just one licence header per file in :" + FILEWARN(path) + ".")
219  return FormatReturn.Error
220 
221  elif licence_number < 1:
222 
223  if enable_reformat:
224 
225  lic = LICENSE
226  lic = lic.replace("(.*)", "%s-%s" % (YEAR, YEAR))
227  lic = lic.replace("\\", "")
228 
229  with open(path, 'wb') as source_file:
230 
231  source_file.write(lic + "\n\n")
232  source_file.write(content)
233 
234  common.note('LGPL license header fixed in : ' + FILEWARN(path) + '.')
235  return FormatReturn.Modified
236 
237  else:
238 
239  common.error("There should be at least one licence header per file in :" + FILEWARN(path) + ".")
240  return FormatReturn.Error
241 
242  # Here, it has only one occurrences that must be checked
243 
244  # Check license
245  LICENSE_YEAR = r"(.*)FW4SPL - Copyright \(C\) IRCAD, ([0-9]+)."
246  LICENSE_YEAR_RANGE = r"(.*)FW4SPL - Copyright \(C\) IRCAD, ([0-9]+)-([0-9]+)."
247 
248  # Check date
249  if re.search(LICENSE_YEAR_RANGE, content):
250 
251  LICENSE_YEAR_REPLACE = r"\1FW4SPL - Copyright (C) IRCAD, \2-" + str(YEAR) + "."
252  str_new_file = re.sub(LICENSE_YEAR_RANGE, LICENSE_YEAR_REPLACE, content)
253 
254  else:
255 
256  match = re.search(LICENSE_YEAR, content)
257 
258  if match:
259 
260  if status == 'A' or match.group(2) == str(YEAR):
261 
262  LICENSE_YEAR_REPLACE = r"\1FW4SPL - Copyright (C) IRCAD, " + str(YEAR) + "."
263  str_new_file = re.sub(LICENSE_YEAR, LICENSE_YEAR_REPLACE, content)
264 
265  else:
266 
267  LICENSE_YEAR_REPLACE = r"\1FW4SPL - Copyright (C) IRCAD, \2-" + str(YEAR) + "."
268  str_new_file = re.sub(LICENSE_YEAR, LICENSE_YEAR_REPLACE, content)
269  else:
270 
271  common.error('Licence year format in : ' + FILEWARN(path) + ' is not correct.')
272  return FormatReturn.Error
273 
274  if str_new_file != content:
275 
276  if enable_reformat:
277 
278  common.note('Licence year fixed in : ' + FILEWARN(path))
279  with open(path, 'wb') as source_file:
280  source_file.write(str_new_file)
281  return FormatReturn.Modified
282 
283  else:
284 
285  common.error('Licence year in : ' + FILEWARN(path) + ' is not up-to-date.')
286  return FormatReturn.Error
287 
288  return FormatReturn.NotModified
289 
290 
291 # ------------------------------------------------------------------------------
292 
293 # Check the header guard consistency
294 def fix_header_guard(path, enable_reformat):
295  ret = FormatReturn()
296 
297  with open(path, 'r') as source_file:
298  content = source_file.read()
299 
300  # Regex for '#pragma once'
301  single_comment = "(\/\/([^(\n|\r)]|\(|\))*)"
302  multi_comment = "(\/\*([^\*\/]|\*[^\/]|\/)*\*\/)"
303  useless_char = "\t| |\r"
304  pragma_once = "#pragma(" + useless_char + ")+once"
305  all_before_pragma = ".*" + pragma_once + "(" + useless_char + ")*\n"
306 
307  # Remove old style
308  path_upper = path.upper()
309  path_upper = path_upper.replace("\\", "/")
310  substrings = path_upper.split('/');
311  find = False;
312  res = "__";
313  for i in range(0, len(substrings)):
314  if substrings[i] == "INCLUDE":
315  find = True;
316  elif substrings[i] == "TEST":
317  res += substrings[i - 1].upper() + "_UT_";
318  elif find:
319  res += substrings[i].upper() + "_";
320  expected_guard = res.split('.');
321  expected_guard[0] += "_HPP__";
322 
323  expected_guard = expected_guard[0]
324 
325  # Remove all about expected guard
326  while len(re.findall("#(ifndef|define|endif)((" + useless_char + ")|(/\*)|(\/\/))*" + expected_guard + "[^\n]*",
327  content, re.DOTALL)) != 0:
328 
329  match2 = re.search("#(ifndef|define|endif)((" + useless_char + ")|(/\*)|(\/\/))*" + expected_guard + "[^\n]*",
330  content, re.DOTALL)
331  if enable_reformat:
332 
333  content = content.replace(match2.group(0), "")
334  common.note("Old style of header guard fixed : " + match2.group(0) + "in file : " + FILEWARN(path) + ".")
335  with open(path, 'wb') as source_file:
336  source_file.write(content)
337  ret.add(FormatReturn.Modified)
338 
339  else:
340 
341  common.error("Old style of header guard found : " + match2.group(0) + "in file : " + FILEWARN(path) + ".")
342  ret.add(FormatReturn.Error)
343  return ret.value
344 
345  # Number of occurrences of '#pragma once'
346  pragma_number = len(re.findall(pragma_once, content, re.MULTILINE))
347  if pragma_number > 1:
348 
349  common.error("There should be just one '#pragma once' per file in :" + FILEWARN(path) + ".")
350  ret.add(FormatReturn.Error)
351  return ret.value
352 
353  elif pragma_number < 1:
354 
355  # Add 'pragma once'
356  if enable_reformat:
357 
358  match = re.search("(" + single_comment + "|" + multi_comment + "|" + useless_char + "|\n)*", content,
359  re.MULTILINE)
360 
361  with open(path, 'wb') as source_file:
362  source_file.write(match.group(0))
363  source_file.write("#pragma once\n\n")
364  source_file.write(content.replace(match.group(0), ""))
365 
366  common.note("'#pragma once' fixed in :" + FILEWARN(path))
367 
368  ret.add(FormatReturn.Modified)
369  return ret.value
370 
371  else:
372 
373  common.error("There should be at least one '#pragma once' per file in :" + FILEWARN(path) + ".")
374  ret.add(FormatReturn.Error)
375  return ret.value
376 
377  # Here, it has only one occurrences that must be checked
378 
379  # Get all string before first '#pragma once'
380  out = re.search(all_before_pragma, content, re.DOTALL).group(0)
381 
382  # Remove '#pragma once'
383  match2 = re.search(pragma_once, out, re.DOTALL)
384  out = out.replace(match2.group(0), "")
385 
386  # Remove multi line comments
387  while len(re.findall(multi_comment, out, re.DOTALL)) != 0:
388  match2 = re.search(multi_comment, out, re.DOTALL)
389  out = out.replace(match2.group(0), "")
390 
391  # Remove single line comments
392  while len(re.findall(single_comment, out, re.DOTALL)) != 0:
393  match2 = re.search(single_comment, out, re.DOTALL)
394  out = out.replace(match2.group(0), "")
395 
396  # If it's not empty, they are an error
397  if len(re.findall("[^\n]", out, re.DOTALL)) != 0:
398  common.error(
399  ("Unexpected : '%s' befor #pragma once in :" % re.search("^.+$", out, re.MULTILINE).group(0)) + FILEWARN(
400  path) + ".")
401  ret.add(FormatReturn.Error)
402  return ret.value
403 
404  # Check space number between '#pragma' and 'once'
405  if len(re.findall("#pragma once", content, re.DOTALL)) == 0:
406 
407  if enable_reformat:
408  # Get all string before first '#pragma once'
409  out = re.search(all_before_pragma, content, re.DOTALL).group(0)
410 
411  # Remove '#pragma once'
412  match2 = re.search(pragma_once, out, re.DOTALL)
413  out2 = out.replace(match2.group(0), "")
414 
415  with open(path, 'wb') as source_file:
416 
417  source_file.write(out2)
418  source_file.write("#pragma once\n")
419  source_file.write(content.replace(out, ""))
420 
421  ret.add(FormatReturn.Modified)
422  return ret.value
423 
424  else:
425 
426  common.error("Needed : '#pragma once', actual : '" + re.search(pragma_once, content, re.DOTALL).group(
427  0) + "' in file :" + FILEWARN(path) + ".")
428  ret.add(FormatReturn.Error)
429  return ret.value
430 
431  ret.add(FormatReturn.NotModified)
432  return ret.value
def get_repo_root()
Definition: common.py:71
def error(msg)
Definition: common.py:52
def binary(s)
Definition: common.py:60
def get_file_datetime(path, check_commits_date)
Definition: common.py:107
def trace(msg)
Definition: common.py:47
def note(msg)
Definition: common.py:43
def find_libraries_and_bundles(fw4spl_projects)
Definition: sortincludes.py:47
def execute_command(proc)
Definition: common.py:132
def get_option(option, default, type="")
Definition: common.py:150
def sort_includes(path, enable_reformat)
Definition: sortincludes.py:94
def warn(msg)
Definition: common.py:56