summary refs log tree commit diff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-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
4 files changed, 112 insertions, 16 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 3868f94bb2..77724a2448 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -19,6 +19,7 @@ from . import hello
 from . import info
 from . import json
 from . import json2c
+from . import lint
 from . import list
 from . import kle2json
 from . import new
diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py
new file mode 100644
index 0000000000..74467021e0
--- /dev/null
+++ b/lib/python/qmk/cli/lint.py
@@ -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
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index e92c3335b9..d73ba8cfb6 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -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,7 +131,7 @@ 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))
@@ -135,7 +139,7 @@ def _find_all_layouts(keyboard, rules):
     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])
diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py
index 591fad034b..54def1d5d6 100644
--- a/lib/python/qmk/path.py
+++ b/lib/python/qmk/path.py
@@ -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):