From 349d9e6f0777e4275884d6a6af75997844afe541 Mon Sep 17 00:00:00 2001 From: Adam Birds Date: Fri, 30 Apr 2021 21:06:00 +0100 Subject: [PATCH] lint: Add linter rules for markdown. --- .gitignore | 3 +- tools/__init__.py | 0 tools/lib/__init__.py | 0 tools/lib/custom_check.py | 53 ++++++++++++++++++ tools/lib/gitlint-rules.py | 110 +++++++++++++++++++++++++++++++++++++ tools/lint | 9 +++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tools/__init__.py create mode 100644 tools/lib/__init__.py create mode 100644 tools/lib/custom_check.py create mode 100644 tools/lib/gitlint-rules.py diff --git a/.gitignore b/.gitignore index cdeb326..c026eb2 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,5 @@ dmypy.json # Exclusions -!src/xkcd_pass/lib/ \ No newline at end of file +!src/xkcd_pass/lib/ +!tools/lib \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/lib/__init__.py b/tools/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/lib/custom_check.py b/tools/lib/custom_check.py new file mode 100644 index 0000000..d0d464d --- /dev/null +++ b/tools/lib/custom_check.py @@ -0,0 +1,53 @@ +from typing import List + +from zulint.custom_rules import Rule, RuleList + +trailing_whitespace_rule: "Rule" = { + "pattern": r"\s+$", + "strip": "\n", + "description": "Fix trailing whitespace", +} +whitespace_rules: List["Rule"] = [ + # This linter should be first since bash_rules depends on it. + trailing_whitespace_rule, +] + +markdown_whitespace_rules: List["Rule"] = [ + *(rule for rule in whitespace_rules if rule["pattern"] != r"\s+$"), + # Two spaces trailing a line with other content is okay--it's a Markdown line break. + # This rule finds one space trailing a non-space, three or more trailing spaces, and + # spaces on an empty line. + { + "pattern": r"((?[^\]]+)\]\((?P=url)\)", + "description": "Linkified Markdown URLs should use cleaner syntax.", + }, + { + "pattern": r"\][(][^#h]", + "include_only": {"README.md"}, + "description": "Use absolute links from docs served by GitHub", + }, + ], + max_length=120, +) + +non_py_rules = [ + markdown_rules, +] diff --git a/tools/lib/gitlint-rules.py b/tools/lib/gitlint-rules.py new file mode 100644 index 0000000..ff187a1 --- /dev/null +++ b/tools/lib/gitlint-rules.py @@ -0,0 +1,110 @@ +from typing import List + +from gitlint.git import GitCommit +from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation + +# Word list from https://github.com/m1foley/fit-commit +# Copyright (c) 2015 Mike Foley +# License: MIT +# Ref: fit_commit/validators/tense.rb +TENSE_DATA = [ + (["adds", "adding", "added"], "add"), + (["allows", "allowing", "allowed"], "allow"), + (["amends", "amending", "amended"], "amend"), + (["bumps", "bumping", "bumped"], "bump"), + (["calculates", "calculating", "calculated"], "calculate"), + (["changes", "changing", "changed"], "change"), + (["cleans", "cleaning", "cleaned"], "clean"), + (["commits", "committing", "committed"], "commit"), + (["corrects", "correcting", "corrected"], "correct"), + (["creates", "creating", "created"], "create"), + (["darkens", "darkening", "darkened"], "darken"), + (["disables", "disabling", "disabled"], "disable"), + (["displays", "displaying", "displayed"], "display"), + (["documents", "documenting", "documented"], "document"), + (["drys", "drying", "dryed"], "dry"), + (["ends", "ending", "ended"], "end"), + (["enforces", "enforcing", "enforced"], "enforce"), + (["enqueues", "enqueuing", "enqueued"], "enqueue"), + (["extracts", "extracting", "extracted"], "extract"), + (["finishes", "finishing", "finished"], "finish"), + (["fixes", "fixing", "fixed"], "fix"), + (["formats", "formatting", "formatted"], "format"), + (["guards", "guarding", "guarded"], "guard"), + (["handles", "handling", "handled"], "handle"), + (["hides", "hiding", "hid"], "hide"), + (["increases", "increasing", "increased"], "increase"), + (["ignores", "ignoring", "ignored"], "ignore"), + (["implements", "implementing", "implemented"], "implement"), + (["improves", "improving", "improved"], "improve"), + (["keeps", "keeping", "kept"], "keep"), + (["kills", "killing", "killed"], "kill"), + (["makes", "making", "made"], "make"), + (["merges", "merging", "merged"], "merge"), + (["moves", "moving", "moved"], "move"), + (["permits", "permitting", "permitted"], "permit"), + (["prevents", "preventing", "prevented"], "prevent"), + (["pushes", "pushing", "pushed"], "push"), + (["rebases", "rebasing", "rebased"], "rebase"), + (["refactors", "refactoring", "refactored"], "refactor"), + (["removes", "removing", "removed"], "remove"), + (["renames", "renaming", "renamed"], "rename"), + (["reorders", "reordering", "reordered"], "reorder"), + (["replaces", "replacing", "replaced"], "replace"), + (["requires", "requiring", "required"], "require"), + (["restores", "restoring", "restored"], "restore"), + (["sends", "sending", "sent"], "send"), + (["sets", "setting"], "set"), + (["separates", "separating", "separated"], "separate"), + (["shows", "showing", "showed"], "show"), + (["simplifies", "simplifying", "simplified"], "simplify"), + (["skips", "skipping", "skipped"], "skip"), + (["sorts", "sorting"], "sort"), + (["speeds", "speeding", "sped"], "speed"), + (["starts", "starting", "started"], "start"), + (["supports", "supporting", "supported"], "support"), + (["takes", "taking", "took"], "take"), + (["testing", "tested"], "test"), # "tests" excluded to reduce false negatives + (["truncates", "truncating", "truncated"], "truncate"), + (["updates", "updating", "updated"], "update"), + (["uses", "using", "used"], "use"), +] + +TENSE_CORRECTIONS = {word: imperative for words, imperative in TENSE_DATA for word in words} + + +class ImperativeMood(LineRule): + """This rule will enforce that the commit message title uses imperative + mood. This is done by checking if the first word is in `WORD_SET`, if so + show the word in the correct mood.""" + + name = "title-imperative-mood" + id = "Z1" + target = CommitMessageTitle + + error_msg = ( + "The first word in commit title should be in imperative mood " + '("{word}" -> "{imperative}"): "{title}"' + ) + + def validate(self, line: str, commit: GitCommit) -> List[RuleViolation]: + violations = [] + + # Ignore the section tag (ie `
: .`) + words = line.split(": ", 1)[-1].split() + first_word = words[0].lower() + + if first_word in TENSE_CORRECTIONS: + imperative = TENSE_CORRECTIONS[first_word] + violation = RuleViolation( + self.id, + self.error_msg.format( + word=first_word, + imperative=imperative, + title=commit.message.title, + ), + ) + + violations.append(violation) + + return violations diff --git a/tools/lint b/tools/lint index 8ed98d6..a3be7fa 100755 --- a/tools/lint +++ b/tools/lint @@ -4,6 +4,7 @@ from __future__ import absolute_import, print_function import argparse import re +from lib.custom_check import non_py_rules from zulint.command import LinterConfig, add_default_linter_arguments from zulint.custom_rules import RuleList from zulint.linters import run_pyflakes @@ -92,6 +93,14 @@ def run(): failed = trailing_whitespace_rule.check(by_lang, verbose=args.verbose) return 1 if failed else 0 + @linter_config.lint + def custom_nonpy() -> int: + """Runs custom checks for non-python files (config: tools/lib/custom_check.py)""" + failed = False + for rule in non_py_rules: + failed = failed or rule.check(by_lang, verbose=args.verbose) + return 1 if failed else 0 + @linter_config.lint def pyflakes(): # type: () -> int