c_parse.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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'] not in matrix_locations:
  69. cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has no matrix position!', file, key['label'], macro_name)
  70. elif len(matrix_locations.get(key['label'])) > 1:
  71. cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has multiple matrix positions (%s)', file, key['label'], macro_name, ', '.join(str(x) for x in matrix_locations[key['label']]))
  72. else:
  73. key['matrix'] = matrix_locations[key['label']][0]
  74. parsed_layouts[macro_name] = {
  75. 'layout': parsed_layout,
  76. 'filename': str(file),
  77. }
  78. elif '#define' in line:
  79. # Attempt to extract a new layout alias
  80. try:
  81. _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
  82. aliases[pp_macro_name] = pp_macro_text
  83. except ValueError:
  84. continue
  85. return parsed_layouts, aliases
  86. def parse_config_h_file(config_h_file, config_h=None):
  87. """Extract defines from a config.h file.
  88. """
  89. if not config_h:
  90. config_h = {}
  91. config_h_file = Path(config_h_file)
  92. if config_h_file.exists():
  93. config_h_text = config_h_file.read_text(encoding='utf-8')
  94. config_h_text = config_h_text.replace('\\\n', '')
  95. config_h_text = strip_multiline_comment(config_h_text)
  96. for linenum, line in enumerate(config_h_text.split('\n')):
  97. line = strip_line_comment(line).strip()
  98. if not line:
  99. continue
  100. line = line.split()
  101. if line[0] == '#define':
  102. if len(line) == 1:
  103. cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
  104. elif len(line) == 2:
  105. config_h[line[1]] = True
  106. else:
  107. config_h[line[1]] = ' '.join(line[2:])
  108. elif line[0] == '#undef':
  109. if len(line) == 2:
  110. if line[1] in config_h:
  111. if config_h[line[1]] is True:
  112. del config_h[line[1]]
  113. else:
  114. config_h[line[1]] = False
  115. else:
  116. cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
  117. return config_h
  118. def _default_key(label=None):
  119. """Increment x and return a copy of the default_key_entry.
  120. """
  121. default_key_entry['x'] += 1
  122. new_key = default_key_entry.copy()
  123. if label:
  124. new_key['label'] = label
  125. return new_key
  126. def _parse_layout_macro(layout_macro):
  127. """Split the LAYOUT macro into its constituent parts
  128. """
  129. layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
  130. macro_name, layout = layout_macro.split('(', 1)
  131. layout, matrix = layout.split(')', 1)
  132. return macro_name, layout, matrix
  133. def _parse_matrix_locations(matrix, file, macro_name):
  134. """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
  135. """
  136. matrix_locations = {}
  137. for row_num, row in enumerate(matrix.split('},{')):
  138. if row.startswith('LAYOUT'):
  139. cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
  140. break
  141. row = row.replace('{', '').replace('}', '')
  142. for col_num, identifier in enumerate(row.split(',')):
  143. if identifier != 'KC_NO':
  144. if identifier not in matrix_locations:
  145. matrix_locations[identifier] = []
  146. matrix_locations[identifier].append([row_num, col_num])
  147. return matrix_locations
  148. def _coerce_led_token(_type, value):
  149. """ Convert token to valid info.json content
  150. """
  151. value_map = {
  152. 'NO_LED': None,
  153. 'LED_FLAG_ALL': 0xFF,
  154. 'LED_FLAG_NONE': 0x00,
  155. 'LED_FLAG_MODIFIER': 0x01,
  156. 'LED_FLAG_UNDERGLOW': 0x02,
  157. 'LED_FLAG_KEYLIGHT': 0x04,
  158. 'LED_FLAG_INDICATOR': 0x08,
  159. }
  160. if _type is Token.Literal.Number.Integer:
  161. return int(value)
  162. if _type is Token.Literal.Number.Float:
  163. return float(value)
  164. if _type is Token.Literal.Number.Hex:
  165. return int(value, 0)
  166. if _type is Token.Name and value in value_map.keys():
  167. return value_map[value]
  168. def _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags):
  169. # TODO: Improve crude parsing/validation
  170. if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
  171. raise ValueError("Unable to parse g_led_config matrix data")
  172. if len(position) != len(flags):
  173. raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})")
  174. if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
  175. raise ValueError(f"LED index {max(matrix_indexes)} is OOB in g_led_config - should be < {len(flags)}")
  176. if not all(isinstance(n, int) for n in matrix_indexes):
  177. raise ValueError("matrix indexes are not all ints")
  178. if (len(position_raw) % 2) != 0:
  179. raise ValueError("Malformed g_led_config position data")
  180. def _parse_led_config(file, matrix_cols, matrix_rows):
  181. """Return any 'raw' led/rgb matrix config
  182. """
  183. matrix_raw = []
  184. position_raw = []
  185. flags = []
  186. found_led_config = False
  187. bracket_count = 0
  188. section = 0
  189. for _type, value in lex(_preprocess_c_file(file), CLexer()):
  190. # Assume g_led_config..stuff..;
  191. if value == 'g_led_config':
  192. found_led_config = True
  193. elif value == ';':
  194. found_led_config = False
  195. elif found_led_config:
  196. # Assume bracket count hints to section of config we are within
  197. if value == '{':
  198. bracket_count += 1
  199. if bracket_count == 2:
  200. section += 1
  201. elif value == '}':
  202. bracket_count -= 1
  203. else:
  204. # Assume any non whitespace value here is important enough to stash
  205. if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
  206. if section == 1 and bracket_count == 3:
  207. matrix_raw.append(_coerce_led_token(_type, value))
  208. if section == 2 and bracket_count == 3:
  209. position_raw.append(_coerce_led_token(_type, value))
  210. if section == 3 and bracket_count == 2:
  211. flags.append(_coerce_led_token(_type, value))
  212. elif _type in [Token.Comment.Preproc]:
  213. # TODO: Promote to error
  214. return None
  215. # Slightly better intrim format
  216. matrix = list(_get_chunks(matrix_raw, matrix_cols))
  217. position = list(_get_chunks(position_raw, 2))
  218. matrix_indexes = list(filter(lambda x: x is not None, matrix_raw))
  219. # If we have not found anything - bail with no error
  220. if not section:
  221. return None
  222. # Throw any validation errors
  223. _validate_led_config(matrix, matrix_rows, matrix_indexes, position, position_raw, flags)
  224. return (matrix, position, flags)
  225. def find_led_config(file, matrix_cols, matrix_rows):
  226. """Search file for led/rgb matrix config
  227. """
  228. found = _parse_led_config(file, matrix_cols, matrix_rows)
  229. if not found:
  230. return None
  231. # Expand collected content
  232. (matrix, position, flags) = found
  233. # Align to output format
  234. led_config = []
  235. for index, item in enumerate(position, start=0):
  236. led_config.append({
  237. 'x': item[0],
  238. 'y': item[1],
  239. 'flags': flags[index],
  240. })
  241. for r in range(len(matrix)):
  242. for c in range(len(matrix[r])):
  243. index = matrix[r][c]
  244. if index is not None:
  245. led_config[index]['matrix'] = [r, c]
  246. return led_config