Skip to content

Commit

Permalink
New command: qmk lint (qmk#10761)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
2 people authored and ChrissiQ committed Nov 10, 2020
1 parent 756bd4a commit cfc9437
Showing 7 changed files with 152 additions and 23 deletions.
9 changes: 3 additions & 6 deletions .github/workflows/info.yml → .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ on:
- 'keyboards/**'

jobs:
info:
lint:
runs-on: ubuntu-latest

container: qmkfm/base_container
@@ -27,7 +27,7 @@ jobs:
echo ${{ github.event.pull_request.base.sha }}
echo '${{ steps.file_changes.outputs.files}}'
- name: Run qmk info
- name: Run qmk lint
shell: 'bash {0}'
run: |
QMK_CHANGES=$(echo -e '${{ steps.file_changes.outputs.files}}')
@@ -45,10 +45,7 @@ jobs:
if [[ $KEYMAP_ONLY -gt 0 ]]; then
echo "linting ${KB}"
# TODO: info info always returns 0 - right now the only way to know failure is to inspect log lines
qmk info -l -kb ${KB} 2>&1 | tee /tmp/$$
!(grep -cq ☒ /tmp/$$)
: $((exit_code = $exit_code + $?))
qmk lint --keyboard ${KB}
fi
done
exit $exit_code
19 changes: 18 additions & 1 deletion docs/cli_commands.md
Original file line number Diff line number Diff line change
@@ -178,6 +178,24 @@ Creates a keymap.json from a keymap.c.
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`

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
```

19 changes: 19 additions & 0 deletions docs/hardware_keyboard_guidelines.md
Original file line number Diff line number Diff line change
@@ -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.


## 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

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.
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
from . import info
from . import json
from . import json2c
from . import lint
from . import list
from . import kle2json
from . import new
70 changes: 70 additions & 0 deletions lib/python/qmk/cli/lint.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 31 additions & 12 deletions lib/python/qmk/info.py
Original file line number Diff line number Diff line change
@@ -28,6 +28,8 @@ def info_json(keyboard):
'keyboard_folder': str(keyboard),
'keymaps': {},
'layouts': {},
'parse_errors': [],
'parse_warnings': [],
'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'}

# 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'):
info_data['layouts'][layout_name] = layout_json

@@ -104,14 +106,16 @@ def _extract_rules_mk(info_data):
mcu = rules.get('MCU')

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 avr_processor_rules(info_data, rules)

return info_data
msg = "Unknown MCU: " + str(mcu)

_log_warning(info_data, msg)

return unknown_processor_rules(info_data, rules)


def _search_keyboard_h(path):
@@ -127,15 +131,15 @@ def _search_keyboard_h(path):
return layouts


def _find_all_layouts(keyboard, rules):
def _find_all_layouts(info_data, keyboard, rules):
"""Looks for layout macros associated with this keyboard.
"""
layouts = _search_keyboard_h(Path(keyboard))

if not layouts:
# 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.
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):
if file.endswith('.h'):
these_layouts = find_layouts(file)
@@ -153,11 +157,25 @@ def _find_all_layouts(keyboard, rules):
supported_layouts.remove(layout_name)

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


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):
"""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)

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

# 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
if layout_name in info_data['layouts']:
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:
for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(json_layout['layout'][i])
14 changes: 10 additions & 4 deletions lib/python/qmk/path.py
Original file line number Diff line number Diff line change
@@ -28,15 +28,21 @@ def under_qmk_firmware():
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.
Args:
keyboard
keyboard_name
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):
if (keyboard_folder / 'keymaps').exists():
@@ -45,7 +51,7 @@ def keymap(keyboard):
keyboard_folder = keyboard_folder.parent

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):

0 comments on commit cfc9437

Please sign in to comment.