aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/lint.yml (renamed from .github/workflows/info.yml)9
-rw-r--r--docs/cli_commands.md19
-rw-r--r--docs/hardware_keyboard_guidelines.md19
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/lint.py70
-rw-r--r--lib/python/qmk/info.py43
-rw-r--r--lib/python/qmk/path.py14
7 files changed, 152 insertions, 23 deletions
diff --git a/.github/workflows/info.yml b/.github/workflows/lint.yml
index bb3a50847..1aa347a1b 100644
--- a/.github/workflows/info.yml
+++ b/.github/workflows/lint.yml
@@ -6,7 +6,7 @@ on:
6 - 'keyboards/**' 6 - 'keyboards/**'
7 7
8jobs: 8jobs:
9 info: 9 lint:
10 runs-on: ubuntu-latest 10 runs-on: ubuntu-latest
11 11
12 container: qmkfm/base_container 12 container: qmkfm/base_container
@@ -27,7 +27,7 @@ jobs:
27 echo ${{ github.event.pull_request.base.sha }} 27 echo ${{ github.event.pull_request.base.sha }}
28 echo '${{ steps.file_changes.outputs.files}}' 28 echo '${{ steps.file_changes.outputs.files}}'
29 29
30 - name: Run qmk info 30 - name: Run qmk lint
31 shell: 'bash {0}' 31 shell: 'bash {0}'
32 run: | 32 run: |
33 QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.files}}') 33 QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.files}}')
@@ -45,10 +45,7 @@ jobs:
45 if [[ $KEYMAP_ONLY -gt 0 ]]; then 45 if [[ $KEYMAP_ONLY -gt 0 ]]; then
46 echo "linting ${KB}" 46 echo "linting ${KB}"
47 47
48 # TODO: info info always returns 0 - right now the only way to know failure is to inspect log lines 48 qmk lint --keyboard ${KB}
49 qmk info -l -kb ${KB} 2>&1 | tee /tmp/$$
50 !(grep -cq ☒ /tmp/$$)
51 : $((exit_code = $exit_code + $?))
52 fi 49 fi
53 done 50 done
54 exit $exit_code 51 exit $exit_code
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index c970b1efa..b10f5d499 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -178,6 +178,24 @@ Creates a keymap.json from a keymap.c.
178qmk c2json [--no-cpp] [-o OUTPUT] filename 178qmk c2json [--no-cpp] [-o OUTPUT] filename
179``` 179```
180 180
181## `qmk lint`
182
183Checks over a keyboard and/or keymap and highlights common errors, problems, and anti-patterns.
184
185**Usage**:
186
187```
188qmk lint [-km KEYMAP] [-kb KEYBOARD] [--strict]
189```
190
191This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
192
193**Examples**:
194
195Do a basic lint check:
196
197 qmk lint -kb rominronin/katana60/rev2
198
181## `qmk list-keyboards` 199## `qmk list-keyboards`
182 200
183This command lists all the keyboards currently defined in `qmk_firmware` 201This command lists all the keyboards currently defined in `qmk_firmware`
@@ -309,4 +327,3 @@ This command runs the python test suite. If you make changes to python code you
309``` 327```
310qmk pytest 328qmk pytest
311``` 329```
312
diff --git a/docs/hardware_keyboard_guidelines.md b/docs/hardware_keyboard_guidelines.md
index d49d0d092..742b56572 100644
--- a/docs/hardware_keyboard_guidelines.md
+++ b/docs/hardware_keyboard_guidelines.md
@@ -3,6 +3,25 @@
3Since starting, QMK has grown by leaps and bounds thanks to people like you who contribute to creating and maintaining our community keyboards. As we've grown we've discovered some patterns that work well, and ask that you conform to them to make it easier for other people to benefit from your hard work. 3Since starting, QMK has grown by leaps and bounds thanks to people like you who contribute to creating and maintaining our community keyboards. As we've grown we've discovered some patterns that work well, and ask that you conform to them to make it easier for other people to benefit from your hard work.
4 4
5 5
6## Use QMK Lint
7
8We have provided a tool, `qmk lint`, which will let you check over your keyboard for problems. We suggest using it frequently while working on your keyboard and keymap.
9
10Example passing check:
11
12```
13$ qmk lint -kb rominronin/katana60/rev2
14Ψ Lint check passed!
15```
16
17Example failing check:
18
19```
20$ qmk lint -kb clueboard/66/rev3
21☒ Missing keyboards/clueboard/66/rev3/readme.md
22☒ Lint check failed!
23```
24
6## Naming Your Keyboard/Project 25## Naming Your Keyboard/Project
7 26
8All keyboard names are in lower case, consisting only of letters, numbers, and underscore (`_`). Names may not begin with an underscore. Forward slash (`/`) is used as a sub-folder separation character. 27All keyboard names are in lower case, consisting only of letters, numbers, and underscore (`_`). Names may not begin with an underscore. Forward slash (`/`) is used as a sub-folder separation character.
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
19from . import info 19from . import info
20from . import json 20from . import json
21from . import json2c 21from . import json2c
22from . import lint
22from . import list 23from . import list
23from . import kle2json 24from . import kle2json
24from . import new 25from . 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"""
3from milc import cli
4
5from qmk.decorators import automagic_keyboard, automagic_keymap
6from qmk.info import info_json
7from qmk.keymap import locate_keymap
8from 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
17def 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
117def _search_keyboard_h(path): 121def _search_keyboard_h(path):
@@ -127,7 +131,7 @@ def _search_keyboard_h(path):
127 return layouts 131 return layouts
128 132
129 133
130def _find_all_layouts(keyboard, rules): 134def _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
165def _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
172def _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
161def arm_processor_rules(info_data, rules): 179def 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
31def keymap(keyboard): 31def 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
37def 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
51def normpath(path): 57def normpath(path):