Before you commit, every commit

An adventure in automated static code analysis

What is static code analysis?

  • "The analysis of computer software that is performed without actually executing programs"
  • Prevent problematic code from being deployed
    • Linting: identify bugs, syntax errors
    • Identify security vulnerabilities
    • Calculate metrics

How do we do static code analysis?

  • Use IDE plugins in your personal development environment
  • Integrate into the CI pipeline
    • SonarQube, pylint, flake8, black, mypy, etc.

Problems with

IDE-based analysis:

  • Doesn't scale well - how many IDEs do we support?
  • Tough to enforce in community/volunteer projects
  • Leaves the door open to individual deviations

Problems with

CI-based analysis:

  • Feedback loop is rather late (often after peer review/merge)
  • If the tool's analysis is asynchronous (SonarQube), it's easy to forget to monitor the results.
  • Automated fixes are a bad idea

super-linter!

Introducing:

from Github

Super-linter pros:

  • Provides earlier feedback loop than the CI pipeline
    • cannot be ignored by rogue developers
  • Supports multiple languages (even in the same repo!)
  • Can be run locally

Super-linter cons (as of June 2020):

  • The only python linter is Pylint
  • No auto-fixing, just alerting

Wouldn't it be cool if...

  • No vendor lock-in
  • Move the feedback loop to the left using git hooks
  • Changes could be automatically applied
    • e.g. automatically replace tabs with spaces
  • Open by design: easily plug in your own checksa

pre-commit

Introducing:

A framework for managing and maintaining multi-language pre-commit* hooks.

What are

pre-commit hooks?

bash$ git init
Initialized empty Git repository in .git/

bash$ ls .git/hooks/
applypatch-msg.sample     pre-applypatch.sample     pre-rebase.sample
commit-msg.sample         pre-commit.sample         pre-receive.sample
fsmonitor-watchman.sample pre-merge-commit.sample   prepare-commit-msg.sample
post-update.sample        pre-push.sample           update.sample

*The pre-commit tool actually supports 10 hook types.

Let's set up a super-linter equivalent using pre-commit

bash$ pip install pre-commit

bash$ cat .pre-commit-config.yaml
repos:
-   repo: https://github.com/PyCQA/pylint
    rev: master
    hooks:
    - id: pylint
    
bash$ pre-commit install

What does `pre-commit install` do?

bash$ head -n 20 .git/hooks/pre-commit

#!/usr/bin/env python
"""File generated by pre-commit: https://pre-commit.com"""
from __future__ import print_function

import distutils.spawn
import os
import subprocess
import sys

# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)

HERE = os.path.dirname(os.path.abspath(__file__))
Z40 = '0' * 40
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
# start templated
CONFIG = '.pre-commit-config.yaml'
HOOK_TYPE = 'pre-commit'
INSTALL_PYTHON = '/usr/local/opt/python/bin/python3.7'
SKIP_ON_MISSING_CONFIG = False

Let's write some beautiful code:

import sys
import os

name = input('What is your name?\n')

x = True
if x:
	print ('Hi, %s.' % name)
	print ("This is a really long line" + ", and that's okay because" + " lines are allowed to be long, right?")
bash$ git commit -m "example commit"
pylint...................................................................Failed
hookid: pylint

************* Module test
foo/test.py:8:0: W0311: Bad indentation. Found 1 spaces, expected 4 (bad-indentation)
foo/test.py:9:0: C0301: Line too long (109/80) (line-too-long)
foo/test.py:9:0: W0311: Bad indentation. Found 1 spaces, expected 4 (bad-indentation)
foo/test.py:11:0: C0305: Trailing newlines (trailing-newlines)
foo/test.py:6:0: C0103: Constant name "x" doesn't conform to '(([A-Z_][A-Z0-9_]*)|(__.*__))$' pattern (invalid-name)
foo/test.py:1:0: W0611: Unused import sys (unused-import)
foo/test.py:2:0: W0611: Unused import os (unused-import)

------------------------------------
Your code has been rated at 0.00/10

Some hooks fix code for us!!

bash$ cat .pre-commit-config.yaml
repos:
    
# black: python code formatting
-   repo: https://github.com/psf/black
    rev: stable
    hooks:
    - id: black

# python import sorting, remove unused imports
-   repo: https://github.com/sqlalchemyorg/zimports/
    rev: 0.2.0
    hooks:
    -   id: zimports

-   repo: https://github.com/PyCQA/pylint
    rev: master
    hooks:
    - id: pylint
bash$ git commit -m "example commit"
black....................................................................Failed
hookid: black

Files were modified by this hook. Additional output:

reformatted foo/test.py
All done! ✨ 🍰 ✨
1 file reformatted.

zimports.................................................................Failed
hookid: zimports

Files were modified by this hook. Additional output:

[Writing]       foo/test.py ([20% of lines are imports] [source +0L/-2L] [2 imports removed in 0.0038 sec])

pylint...................................................................Failed
hookid: pylint

************* Module test
foo/test.py:4:0: C0103: Constant name "x" doesn't conform to '(([A-Z_][A-Z0-9_]*)|(__.*__))$' pattern (invalid-name)

------------------------------------------------------------------
Your code has been rated at 8.00/10 (previous run: 0.00/10, +8.00)
bash$ git diff foo/test.py
diff --git a/foo/test.py b/foo/test.py
index 1b50268..ab066ec 100644
--- a/foo/test.py
+++ b/foo/test.py
@@ -1,11 +1,11 @@
-import sys
-import os

-name = input('What is your name?\n')
+name = input("What is your name?\n")

 x = True
 if x:
-       print ('Hi, %s.' % name)
-       print ("This is a really long line" + ", and that's okay because" + " lines are allowed to be long, right?")
-
-
+    print("Hi, %s." % name)
+    print(
+        "This is a really long line"
+        + ", and that's okay because"
+        + " lines are allowed to be long, right?"
+    )

A "quick" feature summary

  • Open by design, easy to add and create new hooks
  • Supports several *execution platforms*:
    • shell, Docker, Golang, Node, Perl, Ruby, Rust, Swift
    • and Python, obviously
  • Lint more than just programming languages
  • Can run in CI using `pre-commit run --all-files`
  • Many configuration options for tweaking/overriding hook functionality to suit your project.

Pre-commit cons

  • Doesn't support every language natively (notably JVM)
    • but shell and docker are sufficient workarounds
  • Requires installation locally
    • If a developer doesn't install locally, they may push code that deviates from the project norms.
    • Workaround - put pre-commit in the CI pipeline as well.

A quick aside about the creator,

Anthony Sottile

What hooks are available for Python?

  • There are many hooks for Python
    • pylint, flake8, autoflake, black, mypy
    • requirements, jinja, docstrings
    • bandit, safety security tests
  • There are many generic hooks for everyone
    • detect private keys/credentials
    • consistent end of line format
    • check for >>>> merge conflicts
    • enforce commit message format
    • check file permissions

What about my friends that write their code in _____?

There are currently 348 hooks tracked by pre-commit, including hooks for:

 

  • Ansible, Chef, Puppet, Terraform, and Cloudformation
  • Perl, PHP, and Bash
  • Lua, R, and Nim
  • CMake, CPP, Clang, and Makefile
  • Mac Admin (jamf, munki, autopkg)

Pre-commit isn't the only option

*but it has the best community support

  • pre-commit
  • Github super-linter
  • lefthook (ruby first, supports python)
  • husky, lint-staged (javascript only)

The pre-commit hook phase of the developer workflow is a great place to do static code analysis.

 

  • A team-wide standard using configuration as code
  • Provides a faster feedback loop than the CI pipeline

In Conclusion

Before you commit

By m3brown

Before you commit

  • 1,038