c_parse.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. """Functions for working with config.h files.
  2. """
  3. from pygments.lexers.c_cpp import CLexer
  4. from pygments.token import Token
  5. from pygments import lex
  6. from itertools import islice
  7. from pathlib import Path
  8. import re
  9. from milc import cli
  10. from qmk.comment_remover import comment_remover
  11. default_key_entry = {'x': -1, 'y': 0, 'w': 1}
  12. single_comment_regex = re.compile(r'\s+/[/*].*$')
  13. multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
  14. layout_macro_define_regex = re.compile(r'^#\s*define')
  15. def _get_chunks(it, size):
  16. """Break down a collection into smaller parts
  17. """
  18. it = iter(it)
  19. return iter(lambda: tuple(islice(it, size)), ())
  20. def _preprocess_c_file(file):
  21. """Load file and strip comments
  22. """
  23. file_contents = file.read_text(encoding='utf-8')
  24. file_contents = comment_remover(file_contents)
  25. return file_contents.replace('\\\n', '')
  26. def strip_line_comment(string):
  27. """Removes comments from a single line string.
  28. """
  29. return single_comment_regex.sub('', string)
  30. def strip_multiline_comment(string):
  31. """Removes comments from a single line string.
  32. """
  33. return multi_comment_regex.sub('', string)
  34. def c_source_files(dir_names):
  35. """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
  36. Args:
  37. dir_names
  38. List of directories relative to `qmk_firmware`.
  39. """
  40. files = []
  41. for dir in dir_names:
  42. files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
  43. return files
  44. def find_layouts(file):
  45. """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
  46. """
  47. file = Path(file)
  48. aliases = {} # Populated with all `#define`s that aren't functions
  49. parsed_layouts = {}
  50. # Search the file for LAYOUT macros and aliases
  51. file_contents = _preprocess_c_file(file)
  52. for line in file_contents.split('\n'):
  53. if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line:
  54. # We've found a LAYOUT macro
  55. macro_name, layout, matrix = _parse_layout_macro(line.strip())
  56. # Reject bad macro names
  57. if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
  58. continue
  59. # Parse the matrix data
  60. matrix_locations = _parse_matrix_locations(matrix, file, macro_name)
  61. # Parse the layout entries into a basic structure
  62. default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
  63. layout = layout.strip()
  64. parsed_layout = [_default_key(key) for key in layout.split(',')]
  65. for i, key in enumerate(parsed_layout):
  66. if 'label' not in key:
  67. cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
  68. elif key['label'] in matrix_locations:
  69. key['matrix'] = matrix_locations[key['label']]
  70. parsed_layouts[macro_name] = {
  71. 'layout': parsed_layout,
  72. 'filename': str(file),
  73. }
  74. elif '#define' in line:
  75. # Attempt to extract a new layout alias
  76. try:
  77. _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
  78. aliases[pp_macro_name] = pp_macro_text
  79. except ValueError:
  80. continue
  81. return parsed_layouts, aliases
  82. def parse_config_h_file(config_h_file, config_h=None):
  83. """Extract defines from a config.h file.
  84. """
  85. if not config_h:
  86. config_h = {}
  87. config_h_file = Path(config_h_file)
  88. if config_h_file.exists():
  89. config_h_text = config_h_file.read_text(encoding='utf-8')
  90. config_h_text = config_h_text.replace('\\\n', '')
  91. config_h_text = strip_multiline_comment(config_h_text)
  92. for linenum, line in enumerate(config_h_text.split('\n')):
  93. line = strip_line_comment(line).strip()
  94. if not line:
  95. continue
  96. line = line.split()
  97. if line[0] == '#define':
  98. if len(line) == 1:
  99. cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
  100. elif len(line) == 2:
  101. config_h[line[1]] = True
  102. else:
  103. config_h[line[1]] = ' '.join(line[2:])
  104. elif line[0] == '#undef':
  105. if len(line) == 2:
  106. if line[1] in config_h:
  107. if config_h[line[1]] is True:
  108. del config_h[line[1]]
  109. else:
  110. config_h[line[1]] = False
  111. else:
  112. cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
  113. return config_h
  114. def _default_key(label=None):
  115. """Increment x and return a copy of the default_key_entry.
  116. """
  117. default_key_entry['x'] += 1
  118. new_key = default_key_entry.copy()
  119. if label:
  120. new_key['label'] = label
  121. return new_key
  122. def _parse_layout_macro(layout_macro):
  123. """Split the LAYOUT macro into its constituent parts
  124. """
  125. layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
  126. macro_name, layout = layout_macro.split('(', 1)
  127. layout, matrix = layout.split(')', 1)
  128. return macro_name, layout, matrix
  129. def _parse_matrix_locations(matrix, file, macro_name):
  130. """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
  131. """
  132. matrix_locations = {}
  133. for row_num, row in enumerate(matrix.split('},{')):
  134. if row.startswith('LAYOUT'):
  135. cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
  136. break
  137. row = row.replace('{', '').replace('}', '')
  138. for col_num, identifier in enumerate(row.split(',')):
  139. if identifier != 'KC_NO':
  140. matrix_locations[identifier] = [row_num, col_num]
  141. return matrix_locations
  142. def _coerce_led_token(_type, value):
  143. """ Convert token to valid info.json content
  144. """
  145. value_map = {
  146. 'NO_LED': None,
  147. 'LED_FLAG_ALL': 0xFF,
  148. 'LED_FLAG_NONE': 0x00,
  149. 'LED_FLAG_MODIFIER': 0x01,
  150. 'LED_FLAG_UNDERGLOW': 0x02,
  151. 'LED_FLAG_KEYLIGHT': 0x04,
  152. 'LED_FLAG_INDICATOR': 0x08,
  153. }
  154. if _type is Token.Literal.Number.Integer:
  155. return int(value)
  156. if _type is Token.Literal.Number.Float:
  157. return float(value)
  158. if _type is Token.Literal.Number.Hex:
  159. return int(value, 0)
  160. if _type is Token.Name and value in value_map.keys():
  161. return value_map[value]
  162. def _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags):
  163. # TODO: Improve crude parsing/validation
  164. if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
  165. raise ValueError("Unable to parse g_led_config matrix data")
  166. if len(position) != len(flags):
  167. raise ValueError("Unable to parse g_led_config position data")
  168. if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
  169. raise ValueError("OOB within g_led_config matrix data")
  170. if not all(isinstance(n, int) for n in matrix_indexes):
  171. raise ValueError("matrix indexes are not all ints")
  172. if (len(position_raw) % 2) != 0:
  173. raise ValueError("Malformed g_led_config position data")
  174. def _parse_led_config(file, matrix_cols, matrix_rows):
  175. """Return any 'raw' led/rgb matrix config
  176. """
  177. matrix_raw = []
  178. position_raw = []
  179. flags = []
  180. found_led_config = False
  181. bracket_count = 0
  182. section = 0
  183. for _type, value in lex(_preprocess_c_file(file), CLexer()):
  184. # Assume g_led_config..stuff..;
  185. if value == 'g_led_config':
  186. found_led_config = True
  187. elif value == ';':
  188. found_led_config = False
  189. elif found_led_config:
  190. # Assume bracket count hints to section of config we are within
  191. if value == '{':
  192. bracket_count += 1
  193. if bracket_count == 2:
  194. section += 1
  195. elif value == '}':
  196. bracket_count -= 1
  197. else:
  198. # Assume any non whitespace value here is important enough to stash
  199. if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
  200. if section == 1 and bracket_count == 3:
  201. matrix_raw.append(_coerce_led_token(_type, value))
  202. if section == 2 and bracket_count == 3:
  203. position_raw.append(_coerce_led_token(_type, value))
  204. if section == 3 and bracket_count == 2:
  205. flags.append(_coerce_led_token(_type, value))
  206. # Slightly better intrim format
  207. matrix = list(_get_chunks(matrix_raw, matrix_cols))
  208. position = list(_get_chunks(position_raw, 2))
  209. matrix_indexes = list(filter(lambda x: x is not None, matrix_raw))
  210. # If we have not found anything - bail with no error
  211. if not section:
  212. return None
  213. # Throw any validation errors
  214. _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags)
  215. return (matrix, position, flags)
  216. def find_led_config(file, matrix_cols, matrix_rows):
  217. """Search file for led/rgb matrix config
  218. """
  219. found = _parse_led_config(file, matrix_cols, matrix_rows)
  220. if not found:
  221. return None
  222. # Expand collected content
  223. (matrix, position, flags) = found
  224. # Align to output format
  225. led_config = []
  226. for index, item in enumerate(position, start=0):
  227. led_config.append({
  228. 'x': item[0],
  229. 'y': item[1],
  230. 'flags': flags[index],
  231. })
  232. for r in range(len(matrix)):
  233. for c in range(len(matrix[r])):
  234. index = matrix[r][c]
  235. if index is not None:
  236. led_config[index]['matrix'] = [r, c]
  237. return led_config