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>
This commit is contained in:
Zach White 2020-11-07 09:56:08 -08:00 committed by GitHub
parent 7ce5402417
commit 4d33d72975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 24 deletions

View File

@ -6,7 +6,7 @@ on:
- 'keyboards/**' - 'keyboards/**'
jobs: jobs:
info: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: qmkfm/base_container container: qmkfm/base_container
@ -27,7 +27,7 @@ jobs:
echo ${{ github.event.pull_request.base.sha }} echo ${{ github.event.pull_request.base.sha }}
echo '${{ steps.file_changes.outputs.files}}' echo '${{ steps.file_changes.outputs.files}}'
- name: Run qmk info - name: Run qmk lint
shell: 'bash {0}' shell: 'bash {0}'
run: | run: |
QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.files}}') QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.files}}')
@ -45,10 +45,7 @@ jobs:
if [[ $KEYMAP_ONLY -gt 0 ]]; then if [[ $KEYMAP_ONLY -gt 0 ]]; then
echo "linting ${KB}" echo "linting ${KB}"
# TODO: info info always returns 0 - right now the only way to know failure is to inspect log lines qmk lint --keyboard ${KB}
qmk info -l -kb ${KB} 2>&1 | tee /tmp/$$
!(grep -cq ☒ /tmp/$$)
: $((exit_code = $exit_code + $?))
fi fi
done done
exit $exit_code exit $exit_code

View File

@ -178,6 +178,24 @@ Creates a keymap.json from a keymap.c.
qmk c2json [--no-cpp] [-o OUTPUT] filename qmk c2json [--no-cpp] [-o OUTPUT] filename
``` ```
## `qmk lint`
Checks over a keyboard and/or keymap and highlights common errors, problems, and anti-patterns.
**Usage**:
```
qmk lint [-km KEYMAP] [-kb KEYBOARD] [--strict]
```
This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
**Examples**:
Do a basic lint check:
qmk lint -kb rominronin/katana60/rev2
## `qmk list-keyboards` ## `qmk list-keyboards`
This command lists all the keyboards currently defined in `qmk_firmware` This 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
``` ```
qmk pytest qmk pytest
``` ```

View File

@ -3,6 +3,25 @@
Since 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. Since 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.
## Use QMK Lint
We 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.
Example passing check:
```
$ qmk lint -kb rominronin/katana60/rev2
Ψ Lint check passed!
```
Example failing check:
```
$ qmk lint -kb clueboard/66/rev3
☒ Missing keyboards/clueboard/66/rev3/readme.md
☒ Lint check failed!
```
## Naming Your Keyboard/Project ## Naming Your Keyboard/Project
All 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. All 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.

View File

@ -19,6 +19,7 @@ from . import hello
from . import info from . import info
from . import json from . import json
from . import json2c from . import json2c
from . import lint
from . import list from . import list
from . import kle2json from . import kle2json
from . import new from . import new

View File

@ -0,0 +1,70 @@
"""Command to look over a keyboard/keymap and check for common mistakes.
"""
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keymap import locate_keymap
from qmk.path import is_keyboard, keyboard
@cli.argument('--strict', action='store_true', help='Treat warnings as errors.')
@cli.argument('-kb', '--keyboard', help='The keyboard to check.')
@cli.argument('-km', '--keymap', help='The keymap to check.')
@cli.subcommand('Check keyboard and keymap for common mistakes.')
@automagic_keyboard
@automagic_keymap
def lint(cli):
"""Check keyboard and keymap for common mistakes.
"""
if not cli.config.lint.keyboard:
cli.log.error('Missing required argument: --keyboard')
cli.print_help()
return False
if not is_keyboard(cli.config.lint.keyboard):
cli.log.error('No such keyboard: %s', cli.config.lint.keyboard)
return False
# Gather data about the keyboard.
ok = True
keyboard_path = keyboard(cli.config.lint.keyboard)
keyboard_info = info_json(cli.config.lint.keyboard)
readme_path = keyboard_path / 'readme.md'
# Check for errors in the info.json
if keyboard_info['parse_errors']:
ok = False
cli.log.error('Errors found when generating info.json.')
if cli.config.lint.strict and keyboard_info['parse_warnings']:
ok = False
cli.log.error('Warnings found when generating info.json (Strict mode enabled.)')
# Check for a readme.md and warn if it doesn't exist
if not readme_path.exists():
ok = False
cli.log.error('Missing %s', readme_path)
# Keymap specific checks
if cli.config.lint.keymap:
keymap_path = locate_keymap(cli.config.lint.keyboard, cli.config.lint.keymap)
if not keymap_path:
ok = False
cli.log.error("Can't find %s keymap for %s keyboard.", cli.config.lint.keymap, cli.config.lint.keyboard)
else:
keymap_readme = keymap_path.parent / 'readme.md'
if not keymap_readme.exists():
cli.log.warning('Missing %s', keymap_readme)
if cli.config.lint.strict:
ok = False
# Check and report the overall status
if ok:
cli.log.info('Lint check passed!')
return True
cli.log.error('Lint check failed!')
return False

View File

@ -28,6 +28,8 @@ def info_json(keyboard):
'keyboard_folder': str(keyboard), 'keyboard_folder': str(keyboard),
'keymaps': {}, 'keymaps': {},
'layouts': {}, 'layouts': {},
'parse_errors': [],
'parse_warnings': [],
'maintainer': 'qmk', 'maintainer': 'qmk',
} }
@ -36,7 +38,7 @@ def info_json(keyboard):
info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
# Populate layout data # Populate layout data
for layout_name, layout_json in _find_all_layouts(keyboard, rules).items(): for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items():
if not layout_name.startswith('LAYOUT_kc'): if not layout_name.startswith('LAYOUT_kc'):
info_data['layouts'][layout_name] = layout_json info_data['layouts'][layout_name] = layout_json
@ -104,14 +106,16 @@ def _extract_rules_mk(info_data):
mcu = rules.get('MCU') mcu = rules.get('MCU')
if mcu in CHIBIOS_PROCESSORS: if mcu in CHIBIOS_PROCESSORS:
arm_processor_rules(info_data, rules) return arm_processor_rules(info_data, rules)
elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
avr_processor_rules(info_data, rules)
else:
cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
unknown_processor_rules(info_data, rules)
return info_data elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
return avr_processor_rules(info_data, rules)
msg = "Unknown MCU: " + str(mcu)
_log_warning(info_data, msg)
return unknown_processor_rules(info_data, rules)
def _search_keyboard_h(path): def _search_keyboard_h(path):
@ -127,7 +131,7 @@ def _search_keyboard_h(path):
return layouts return layouts
def _find_all_layouts(keyboard, rules): def _find_all_layouts(info_data, keyboard, rules):
"""Looks for layout macros associated with this keyboard. """Looks for layout macros associated with this keyboard.
""" """
layouts = _search_keyboard_h(Path(keyboard)) layouts = _search_keyboard_h(Path(keyboard))
@ -135,7 +139,7 @@ def _find_all_layouts(keyboard, rules):
if not layouts: if not layouts:
# If we didn't find any layouts above we widen our search. This is error # If we didn't find any layouts above we widen our search. This is error
# prone which is why we want to encourage people to follow the standard above. # prone which is why we want to encourage people to follow the standard above.
cli.log.warning('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.')
for file in glob('keyboards/%s/*.h' % keyboard): for file in glob('keyboards/%s/*.h' % keyboard):
if file.endswith('.h'): if file.endswith('.h'):
these_layouts = find_layouts(file) these_layouts = find_layouts(file)
@ -153,11 +157,25 @@ def _find_all_layouts(keyboard, rules):
supported_layouts.remove(layout_name) supported_layouts.remove(layout_name)
if supported_layouts: if supported_layouts:
cli.log.error('%s: Missing LAYOUT() macro for %s' % (keyboard, ', '.join(supported_layouts))) _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
return layouts return layouts
def _log_error(info_data, message):
"""Send an error message to both JSON and the log.
"""
info_data['parse_errors'].append(message)
cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
def _log_warning(info_data, message):
"""Send a warning message to both JSON and the log.
"""
info_data['parse_warnings'].append(message)
cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
def arm_processor_rules(info_data, rules): def arm_processor_rules(info_data, rules):
"""Setup the default info for an ARM board. """Setup the default info for an ARM board.
""" """
@ -216,7 +234,7 @@ def merge_info_jsons(keyboard, info_data):
new_info_data = json.load(info_fd) new_info_data = json.load(info_fd)
if not isinstance(new_info_data, dict): if not isinstance(new_info_data, dict):
cli.log.error("Invalid file %s, root object should be a dictionary.", str(info_file)) _log_error(info_data, "Invalid file %s, root object should be a dictionary.", str(info_file))
continue continue
# Copy whitelisted keys into `info_data` # Copy whitelisted keys into `info_data`
@ -230,7 +248,8 @@ def merge_info_jsons(keyboard, info_data):
# Only pull in layouts we have a macro for # Only pull in layouts we have a macro for
if layout_name in info_data['layouts']: if layout_name in info_data['layouts']:
if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']): if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
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'])) msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
_log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
else: else:
for i, key in enumerate(info_data['layouts'][layout_name]['layout']): for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(json_layout['layout'][i]) key.update(json_layout['layout'][i])

View File

@ -28,15 +28,21 @@ def under_qmk_firmware():
return None return None
def keymap(keyboard): def keyboard(keyboard_name):
"""Returns the path to a keyboard's directory relative to the qmk root.
"""
return Path('keyboards') / keyboard_name
def keymap(keyboard_name):
"""Locate the correct directory for storing a keymap. """Locate the correct directory for storing a keymap.
Args: Args:
keyboard keyboard_name
The name of the keyboard. Example: clueboard/66/rev3 The name of the keyboard. Example: clueboard/66/rev3
""" """
keyboard_folder = Path('keyboards') / keyboard keyboard_folder = keyboard(keyboard_name)
for i in range(MAX_KEYBOARD_SUBFOLDERS): for i in range(MAX_KEYBOARD_SUBFOLDERS):
if (keyboard_folder / 'keymaps').exists(): if (keyboard_folder / 'keymaps').exists():
@ -45,7 +51,7 @@ def keymap(keyboard):
keyboard_folder = keyboard_folder.parent keyboard_folder = keyboard_folder.parent
logging.error('Could not find the keymaps directory!') logging.error('Could not find the keymaps directory!')
raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard) raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard_name)
def normpath(path): def normpath(path):