diff options
author | Zach White <skullydazed@gmail.com> | 2020-11-07 09:56:08 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-07 09:56:08 -0800 |
commit | 4d33d72975f2d63c7b6ff6fd4aa7e0f4c4347583 (patch) | |
tree | afc9f0ebeeefed8405443cbe8636458d6c4c18d2 /lib/python | |
parent | 7ce5402417b0332569bf48cf2c51e412cd35a18a (diff) | |
download | qmk_firmware-4d33d72975f2d63c7b6ff6fd4aa7e0f4c4347583.tar.gz qmk_firmware-4d33d72975f2d63c7b6ff6fd4aa7e0f4c4347583.zip |
New command: qmk lint (#10761)
* Basic qmk lint command
* check for keymap readme
* change the workflow from qmk info to qmk lint
* add a strict mode
* parsing -> parse
* document qmk lint
* small info logging cleanup
* Apply suggestions from code review
Co-authored-by: Ryan <fauxpark@gmail.com>
* honor --strict in more places
* change the job name to lint
Co-authored-by: Ryan <fauxpark@gmail.com>
Diffstat (limited to 'lib/python')
-rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
-rw-r--r-- | lib/python/qmk/cli/lint.py | 70 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 43 | ||||
-rw-r--r-- | lib/python/qmk/path.py | 14 |
4 files changed, 112 insertions, 16 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 3868f94bb..77724a244 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
@@ -19,6 +19,7 @@ from . import hello | |||
19 | from . import info | 19 | from . import info |
20 | from . import json | 20 | from . import json |
21 | from . import json2c | 21 | from . import json2c |
22 | from . import lint | ||
22 | from . import list | 23 | from . import list |
23 | from . import kle2json | 24 | from . import kle2json |
24 | from . import new | 25 | from . import new |
diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py new file mode 100644 index 000000000..74467021e --- /dev/null +++ b/lib/python/qmk/cli/lint.py | |||
@@ -0,0 +1,70 @@ | |||
1 | """Command to look over a keyboard/keymap and check for common mistakes. | ||
2 | """ | ||
3 | from milc import cli | ||
4 | |||
5 | from qmk.decorators import automagic_keyboard, automagic_keymap | ||
6 | from qmk.info import info_json | ||
7 | from qmk.keymap import locate_keymap | ||
8 | from qmk.path import is_keyboard, keyboard | ||
9 | |||
10 | |||
11 | @cli.argument('--strict', action='store_true', help='Treat warnings as errors.') | ||
12 | @cli.argument('-kb', '--keyboard', help='The keyboard to check.') | ||
13 | @cli.argument('-km', '--keymap', help='The keymap to check.') | ||
14 | @cli.subcommand('Check keyboard and keymap for common mistakes.') | ||
15 | @automagic_keyboard | ||
16 | @automagic_keymap | ||
17 | def lint(cli): | ||
18 | """Check keyboard and keymap for common mistakes. | ||
19 | """ | ||
20 | if not cli.config.lint.keyboard: | ||
21 | cli.log.error('Missing required argument: --keyboard') | ||
22 | cli.print_help() | ||
23 | return False | ||
24 | |||
25 | if not is_keyboard(cli.config.lint.keyboard): | ||
26 | cli.log.error('No such keyboard: %s', cli.config.lint.keyboard) | ||
27 | return False | ||
28 | |||
29 | # Gather data about the keyboard. | ||
30 | ok = True | ||
31 | keyboard_path = keyboard(cli.config.lint.keyboard) | ||
32 | keyboard_info = info_json(cli.config.lint.keyboard) | ||
33 | readme_path = keyboard_path / 'readme.md' | ||
34 | |||
35 | # Check for errors in the info.json | ||
36 | if keyboard_info['parse_errors']: | ||
37 | ok = False | ||
38 | cli.log.error('Errors found when generating info.json.') | ||
39 | |||
40 | if cli.config.lint.strict and keyboard_info['parse_warnings']: | ||
41 | ok = False | ||
42 | cli.log.error('Warnings found when generating info.json (Strict mode enabled.)') | ||
43 | |||
44 | # Check for a readme.md and warn if it doesn't exist | ||
45 | if not readme_path.exists(): | ||
46 | ok = False | ||
47 | cli.log.error('Missing %s', readme_path) | ||
48 | |||
49 | # Keymap specific checks | ||
50 | if cli.config.lint.keymap: | ||
51 | keymap_path = locate_keymap(cli.config.lint.keyboard, cli.config.lint.keymap) | ||
52 | |||
53 | if not keymap_path: | ||
54 | ok = False | ||
55 | cli.log.error("Can't find %s keymap for %s keyboard.", cli.config.lint.keymap, cli.config.lint.keyboard) | ||
56 | else: | ||
57 | keymap_readme = keymap_path.parent / 'readme.md' | ||
58 | if not keymap_readme.exists(): | ||
59 | cli.log.warning('Missing %s', keymap_readme) | ||
60 | |||
61 | if cli.config.lint.strict: | ||
62 | ok = False | ||
63 | |||
64 | # Check and report the overall status | ||
65 | if ok: | ||
66 | cli.log.info('Lint check passed!') | ||
67 | return True | ||
68 | |||
69 | cli.log.error('Lint check failed!') | ||
70 | return False | ||
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index e92c3335b..d73ba8cfb 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py | |||
@@ -28,6 +28,8 @@ def info_json(keyboard): | |||
28 | 'keyboard_folder': str(keyboard), | 28 | 'keyboard_folder': str(keyboard), |
29 | 'keymaps': {}, | 29 | 'keymaps': {}, |
30 | 'layouts': {}, | 30 | 'layouts': {}, |
31 | 'parse_errors': [], | ||
32 | 'parse_warnings': [], | ||
31 | 'maintainer': 'qmk', | 33 | 'maintainer': 'qmk', |
32 | } | 34 | } |
33 | 35 | ||
@@ -36,7 +38,7 @@ def info_json(keyboard): | |||
36 | info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} | 38 | info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} |
37 | 39 | ||
38 | # Populate layout data | 40 | # Populate layout data |
39 | for layout_name, layout_json in _find_all_layouts(keyboard, rules).items(): | 41 | for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): |
40 | if not layout_name.startswith('LAYOUT_kc'): | 42 | if not layout_name.startswith('LAYOUT_kc'): |
41 | info_data['layouts'][layout_name] = layout_json | 43 | info_data['layouts'][layout_name] = layout_json |
42 | 44 | ||
@@ -104,14 +106,16 @@ def _extract_rules_mk(info_data): | |||
104 | mcu = rules.get('MCU') | 106 | mcu = rules.get('MCU') |
105 | 107 | ||
106 | if mcu in CHIBIOS_PROCESSORS: | 108 | if mcu in CHIBIOS_PROCESSORS: |
107 | arm_processor_rules(info_data, rules) | 109 | return arm_processor_rules(info_data, rules) |
110 | |||
108 | elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: | 111 | elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: |
109 | avr_processor_rules(info_data, rules) | 112 | return avr_processor_rules(info_data, rules) |
110 | else: | ||
111 | cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu)) | ||
112 | unknown_processor_rules(info_data, rules) | ||
113 | 113 | ||
114 | return info_data | 114 | msg = "Unknown MCU: " + str(mcu) |
115 | |||
116 | _log_warning(info_data, msg) | ||
117 | |||
118 | return unknown_processor_rules(info_data, rules) | ||
115 | 119 | ||
116 | 120 | ||
117 | def _search_keyboard_h(path): | 121 | def _search_keyboard_h(path): |
@@ -127,7 +131,7 @@ def _search_keyboard_h(path): | |||
127 | return layouts | 131 | return layouts |
128 | 132 | ||
129 | 133 | ||
130 | def _find_all_layouts(keyboard, rules): | 134 | def _find_all_layouts(info_data, keyboard, rules): |
131 | """Looks for layout macros associated with this keyboard. | 135 | """Looks for layout macros associated with this keyboard. |
132 | """ | 136 | """ |
133 | layouts = _search_keyboard_h(Path(keyboard)) | 137 | layouts = _search_keyboard_h(Path(keyboard)) |
@@ -135,7 +139,7 @@ def _find_all_layouts(keyboard, rules): | |||
135 | if not layouts: | 139 | if not layouts: |
136 | # If we didn't find any layouts above we widen our search. This is error | 140 | # If we didn't find any layouts above we widen our search. This is error |
137 | # prone which is why we want to encourage people to follow the standard above. | 141 | # prone which is why we want to encourage people to follow the standard above. |
138 | cli.log.warning('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) | 142 | _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.') |
139 | for file in glob('keyboards/%s/*.h' % keyboard): | 143 | for file in glob('keyboards/%s/*.h' % keyboard): |
140 | if file.endswith('.h'): | 144 | if file.endswith('.h'): |
141 | these_layouts = find_layouts(file) | 145 | these_layouts = find_layouts(file) |
@@ -153,11 +157,25 @@ def _find_all_layouts(keyboard, rules): | |||
153 | supported_layouts.remove(layout_name) | 157 | supported_layouts.remove(layout_name) |
154 | 158 | ||
155 | if supported_layouts: | 159 | if supported_layouts: |
156 | cli.log.error('%s: Missing LAYOUT() macro for %s' % (keyboard, ', '.join(supported_layouts))) | 160 | _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts))) |
157 | 161 | ||
158 | return layouts | 162 | return layouts |
159 | 163 | ||
160 | 164 | ||
165 | def _log_error(info_data, message): | ||
166 | """Send an error message to both JSON and the log. | ||
167 | """ | ||
168 | info_data['parse_errors'].append(message) | ||
169 | cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message) | ||
170 | |||
171 | |||
172 | def _log_warning(info_data, message): | ||
173 | """Send a warning message to both JSON and the log. | ||
174 | """ | ||
175 | info_data['parse_warnings'].append(message) | ||
176 | cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message) | ||
177 | |||
178 | |||
161 | def arm_processor_rules(info_data, rules): | 179 | def arm_processor_rules(info_data, rules): |
162 | """Setup the default info for an ARM board. | 180 | """Setup the default info for an ARM board. |
163 | """ | 181 | """ |
@@ -216,7 +234,7 @@ def merge_info_jsons(keyboard, info_data): | |||
216 | new_info_data = json.load(info_fd) | 234 | new_info_data = json.load(info_fd) |
217 | 235 | ||
218 | if not isinstance(new_info_data, dict): | 236 | if not isinstance(new_info_data, dict): |
219 | cli.log.error("Invalid file %s, root object should be a dictionary.", str(info_file)) | 237 | _log_error(info_data, "Invalid file %s, root object should be a dictionary.", str(info_file)) |
220 | continue | 238 | continue |
221 | 239 | ||
222 | # Copy whitelisted keys into `info_data` | 240 | # Copy whitelisted keys into `info_data` |
@@ -230,7 +248,8 @@ def merge_info_jsons(keyboard, info_data): | |||
230 | # Only pull in layouts we have a macro for | 248 | # Only pull in layouts we have a macro for |
231 | if layout_name in info_data['layouts']: | 249 | if layout_name in info_data['layouts']: |
232 | if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']): | 250 | if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']): |
233 | cli.log.error('%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s', info_data['keyboard_folder'], layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])) | 251 | msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s' |
252 | _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))) | ||
234 | else: | 253 | else: |
235 | for i, key in enumerate(info_data['layouts'][layout_name]['layout']): | 254 | for i, key in enumerate(info_data['layouts'][layout_name]['layout']): |
236 | key.update(json_layout['layout'][i]) | 255 | key.update(json_layout['layout'][i]) |
diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py index 591fad034..54def1d5d 100644 --- a/lib/python/qmk/path.py +++ b/lib/python/qmk/path.py | |||
@@ -28,15 +28,21 @@ def under_qmk_firmware(): | |||
28 | return None | 28 | return None |
29 | 29 | ||
30 | 30 | ||
31 | def keymap(keyboard): | 31 | def keyboard(keyboard_name): |
32 | """Returns the path to a keyboard's directory relative to the qmk root. | ||
33 | """ | ||
34 | return Path('keyboards') / keyboard_name | ||
35 | |||
36 | |||
37 | def keymap(keyboard_name): | ||
32 | """Locate the correct directory for storing a keymap. | 38 | """Locate the correct directory for storing a keymap. |
33 | 39 | ||
34 | Args: | 40 | Args: |
35 | 41 | ||
36 | keyboard | 42 | keyboard_name |
37 | The name of the keyboard. Example: clueboard/66/rev3 | 43 | The name of the keyboard. Example: clueboard/66/rev3 |
38 | """ | 44 | """ |
39 | keyboard_folder = Path('keyboards') / keyboard | 45 | keyboard_folder = keyboard(keyboard_name) |
40 | 46 | ||
41 | for i in range(MAX_KEYBOARD_SUBFOLDERS): | 47 | for i in range(MAX_KEYBOARD_SUBFOLDERS): |
42 | if (keyboard_folder / 'keymaps').exists(): | 48 | if (keyboard_folder / 'keymaps').exists(): |
@@ -45,7 +51,7 @@ def keymap(keyboard): | |||
45 | keyboard_folder = keyboard_folder.parent | 51 | keyboard_folder = keyboard_folder.parent |
46 | 52 | ||
47 | logging.error('Could not find the keymaps directory!') | 53 | logging.error('Could not find the keymaps directory!') |
48 | raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard) | 54 | raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard_name) |
49 | 55 | ||
50 | 56 | ||
51 | def normpath(path): | 57 | def normpath(path): |