lint.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. #!/usr/bin/env python3
  2. import multiprocessing
  3. import os
  4. import re
  5. import shutil
  6. import subprocess
  7. from flipper.app import App
  8. SOURCE_CODE_FILE_EXTENSIONS = [".h", ".c", ".cpp", ".cxx", ".hpp"]
  9. SOURCE_CODE_FILE_PATTERN = r"^[0-9A-Za-z_]+\.[a-z]+$"
  10. SOURCE_CODE_DIR_PATTERN = r"^[0-9A-Za-z_]+$"
  11. class Main(App):
  12. def init(self):
  13. self.subparsers = self.parser.add_subparsers(help="sub-command help")
  14. # generate
  15. self.parser_check = self.subparsers.add_parser(
  16. "check", help="Check source code format and file names"
  17. )
  18. self.parser_check.add_argument("input", nargs="+")
  19. self.parser_check.set_defaults(func=self.check)
  20. # merge
  21. self.parser_format = self.subparsers.add_parser(
  22. "format", help="Format source code and fix file names"
  23. )
  24. self.parser_format.add_argument(
  25. "input",
  26. nargs="+",
  27. )
  28. self.parser_format.set_defaults(func=self.format)
  29. @staticmethod
  30. def _filter_lint_directories(dirnames: list[str]):
  31. # Skipping 3rd-party code - usually resides in subfolder "lib"
  32. if "lib" in dirnames:
  33. dirnames.remove("lib")
  34. # Skipping hidden folders
  35. for dirname in dirnames.copy():
  36. if dirname.startswith("."):
  37. dirnames.remove(dirname)
  38. def _check_folders(self, folders: list):
  39. show_message = False
  40. pattern = re.compile(SOURCE_CODE_DIR_PATTERN)
  41. for folder in folders:
  42. for dirpath, dirnames, filenames in os.walk(folder):
  43. self._filter_lint_directories(dirnames)
  44. for dirname in dirnames:
  45. if not pattern.match(dirname):
  46. to_fix = os.path.join(dirpath, dirname)
  47. self.logger.warning(f"Found incorrectly named folder {to_fix}")
  48. show_message = True
  49. if show_message:
  50. self.logger.warning(
  51. "Folders are not renamed automatically, please fix it by yourself"
  52. )
  53. def _find_sources(self, folders: list):
  54. output = []
  55. for folder in folders:
  56. for dirpath, dirnames, filenames in os.walk(folder):
  57. self._filter_lint_directories(dirnames)
  58. for filename in filenames:
  59. ext = os.path.splitext(filename.lower())[1]
  60. if ext not in SOURCE_CODE_FILE_EXTENSIONS:
  61. continue
  62. output.append(os.path.join(dirpath, filename))
  63. return output
  64. @staticmethod
  65. def _format_source(task):
  66. try:
  67. subprocess.check_call(task)
  68. return True
  69. except subprocess.CalledProcessError:
  70. return False
  71. def _format_sources(self, sources: list, dry_run: bool = False):
  72. args = ["clang-format", "--Werror", "--style=file", "-i"]
  73. if dry_run:
  74. args.append("--dry-run")
  75. files_per_task = 69
  76. tasks = []
  77. while len(sources) > 0:
  78. tasks.append(args + sources[:files_per_task])
  79. sources = sources[files_per_task:]
  80. pool = multiprocessing.Pool()
  81. results = pool.map(self._format_source, tasks)
  82. return all(results)
  83. def _fix_filename(self, filename: str):
  84. return filename.replace("-", "_")
  85. def _replace_occurrence(self, sources: list, old: str, new: str):
  86. old = old.encode()
  87. new = new.encode()
  88. for source in sources:
  89. content = open(source, "rb").read()
  90. if content.count(old) > 0:
  91. self.logger.info(f"Replacing {old} with {new} in {source}")
  92. content = content.replace(old, new)
  93. open(source, "wb").write(content)
  94. def _apply_file_naming_convention(self, sources: list, dry_run: bool = False):
  95. pattern = re.compile(SOURCE_CODE_FILE_PATTERN)
  96. good = []
  97. bad = []
  98. # Check sources for invalid filenames
  99. for source in sources:
  100. basename = os.path.basename(source)
  101. if not pattern.match(basename):
  102. new_basename = self._fix_filename(basename)
  103. if not pattern.match(new_basename):
  104. self.logger.error(f"Unable to fix name for {basename}")
  105. return False
  106. bad.append((source, basename, new_basename))
  107. else:
  108. good.append(source)
  109. # Notify about errors or replace all occurrences
  110. if dry_run:
  111. if len(bad) > 0:
  112. self.logger.error(f"Found {len(bad)} incorrectly named files")
  113. self.logger.info(bad)
  114. return False
  115. else:
  116. # Replace occurrences in text files
  117. for source, old, new in bad:
  118. self._replace_occurrence(sources, old, new)
  119. # Rename files
  120. for source, old, new in bad:
  121. shutil.move(source, source.replace(old, new))
  122. return True
  123. def _apply_file_permissions(self, sources: list, dry_run: bool = False):
  124. execute_permissions = 0o111
  125. re.compile(SOURCE_CODE_FILE_PATTERN)
  126. good = []
  127. bad = []
  128. # Check sources for unexpected execute permissions
  129. for source in sources:
  130. st = os.stat(source)
  131. perms_too_many = st.st_mode & execute_permissions
  132. if perms_too_many:
  133. good_perms = st.st_mode & ~perms_too_many
  134. bad.append((source, oct(perms_too_many), good_perms))
  135. else:
  136. good.append(source)
  137. # Notify or fix
  138. if dry_run:
  139. if len(bad) > 0:
  140. self.logger.error(f"Found {len(bad)} incorrect permissions")
  141. self.logger.info([record[0:2] for record in bad])
  142. return False
  143. else:
  144. for source, perms_too_many, new_perms in bad:
  145. os.chmod(source, new_perms)
  146. return True
  147. def _perform(self, dry_run: bool):
  148. result = 0
  149. sources = self._find_sources(self.args.input)
  150. if not self._format_sources(sources, dry_run=dry_run):
  151. result |= 0b001
  152. if not self._apply_file_naming_convention(sources, dry_run=dry_run):
  153. result |= 0b010
  154. if not self._apply_file_permissions(sources, dry_run=dry_run):
  155. result |= 0b100
  156. self._check_folders(self.args.input)
  157. return result
  158. def check(self):
  159. return self._perform(dry_run=True)
  160. def format(self):
  161. return self._perform(dry_run=False)
  162. if __name__ == "__main__":
  163. Main()()