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>master
parent
7ce5402417
commit
4d33d72975
|
@ -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
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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])
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue