diff --git a/CHANGES.rst b/CHANGES.rst index bd71075..049d05f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,9 @@ -0.8.11 (unreleased) -------------------- +1.0.0 (unreleased) +------------------ -- Nothing changed yet. +- |backward-incompatible| A new **check-future-tags** command is + introduced that reports orphan future tags. **check-fixmes** does + **not** report those tags anymore, it only reports outdated FIXME's. 0.8.10 (2023-08-16) diff --git a/Makefile b/Makefile index 4802b6e..d5c611f 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ quality: pylint --reports=no setup.py src/check_oldies tests check-branches check-fixmes + check-future-tags python setup.py sdist >/dev/null 2>&1 && twine check dist/* .PHONY: clean diff --git a/docs/check_fixmes.rst b/docs/check_fixmes.rst index 1ecd2f9..b61ada6 100644 --- a/docs/check_fixmes.rst +++ b/docs/check_fixmes.rst @@ -5,17 +5,11 @@ check-fixmes Features and usage ================== -There are two main features: +This command detects old annotations: FIXME, TODO, etc. -- detection of old annotations (FIXME, TODO, etc.); -- detection of forgotten FUTURE tags. - - -Detection of old annotations ----------------------------- Rationale and principles -........................ +------------------------ Developers use annotations to indicate that some code is deficient: FIXME, TODO, OPTIMIZE, HACK, BUG, etc. Semantics vary, but the overall @@ -46,7 +40,7 @@ used. Usage and possible customization -................................ +-------------------------------- .. code-block:: console @@ -96,64 +90,19 @@ be an annotation, but not in this context. For example, in a CSS file: } If you need to ignore whole files, see the :ref:`whitelist option -`. +`. Possible customizations: - which type of annotations are taken in account (FIXME, TODO, OPTIMIZE, etc.): see the :ref:`annotations option - `; + `; - how assignments are formatted: see the the :ref:`assignee_regex - option `; + option `; - the age above which an annotation is considered old: see the - :ref:`max-age option `; - -See the :ref:`check_fixmes_configuration` section below for full details. - - -Detection of orphan FUTURE tags -------------------------------- - -Rationale and principles -........................ - -Developers sometimes plan a broad modification that will span multiple -files. Instead of littering FIXME annotations everywhere, they can set -a single FIXME annotation and a FUTURE-xxx tag on the same line. Then, -wherever we need to make a modification, we only mention this -FUTURE-xxx tag without any FIXME. If we have to "postpone" a FIXME, -there is only line to touch. - -Example: - -.. code-block:: text - - # in file1.py: - # - # FIXME (jsmith, FUTURE-SWITCH-TO-V2): remove this field when we switch to v2 - # - # in file2.py: - # - # FUTURE-SWITCH-TO-V2: deprecate usage when we switch to v2 - -If we ever remove the FIXME but keep the FUTURE-SWITCH-TO-V2 tag in -``file2.py``, it is a mistake and **check-fixmes** warns us. - - -Usage and possible customization -................................ - -**check-fixmes** looks for tags that start with ``FUTURE-`` -(e.g. ``FUTURE-SWITCH-TO-V2``) to make sure that at least one of them -appears on the same line as an annotation. If not, it is considered an -orphan tag and is reported as an error. - -As for annotations, you can ignore a line by using -``no-check-fixmes``, and ignore whole files with the :ref:`whitelist -option `. You can configure how tags are detected with -the :ref:`future_tag_regex option `. + :ref:`max-age option `; See the :ref:`check_fixmes_configuration` section below for full details. @@ -195,7 +144,7 @@ configuration file: Input options ------------- -.. _conf_path: +.. _check_fixmes_conf_path: ``path`` (overridable via the command line) ........................................... @@ -208,7 +157,7 @@ annotations (recursively). It must be a Git checkout repository. | Example: ``path = "src"``. -.. _conf_whitelist: +.. _check_fixmes_conf_whitelist: ``whitelist`` ............. @@ -224,7 +173,7 @@ whitelist whole files by providing a list of glob patterns. Output options -------------- -.. _conf_colorize_errors: +.. _check_fixmes_conf_colorize_errors: ``colorize-errors`` ................... @@ -238,7 +187,7 @@ default foreground color. | Example: ``colorize-errors = false``. -.. _conf_xunit_file: +.. _check_fixmes_conf_xunit_file: ``xunit-file`` (overridable via the command line) ................................................. @@ -255,7 +204,7 @@ exist. Detection options ----------------- -.. _conf_annotations: +.. _check_fixmes_conf_annotations: ``annotations`` ............... @@ -269,7 +218,7 @@ case insensitive: by default, both "todo", "TODO", "fixme" and | Example: ``annotations = ["todo", "optimize", "fixme", "hack"]``. -.. _conf_assignee_regex: +.. _check_fixmes_conf_assignee_regex: ``assignee-regex`` .................. @@ -290,31 +239,7 @@ assignee in an annotation. Requirements: .. _Python syntax: https://docs.python.org/3/library/re.html#regular-expression-syntax -.. _conf_future_tag_regex: - -``future-tag-regex`` -.................... - -The extended regular expression to use to detect FUTURE tags. - -| Type: string (an extended regular expression). -| Default: ``"FUTURE-[-[:alnum:]\._]+?"``. -| Example: ``future-tag-regex = "HEREAFTER-[-[:alnum:]\._]+?"``. - -.. _conf_ignored_orphans_annotations: - -``ignored_orphans_annotations`` -............................... - -The list of annotations which will not trigger orphan FUTURE tags checks. -Note that **check-fixmes** is case insensitive: -by default, both "wontfix", "WONTFIX" will be ignored. - -| Type: list. -| Default: ``["wontfix", "xxx"]`` (case insensitive). -| Example: ``ignored_annotations = ["wontfix", "nofix"]``. - -.. _conf_max_age: +.. _check_fixmes_conf_max_age: ``max-age`` (overridable via the command line) .............................................. diff --git a/docs/check_future_tags.rst b/docs/check_future_tags.rst new file mode 100644 index 0000000..3cd6593 --- /dev/null +++ b/docs/check_future_tags.rst @@ -0,0 +1,186 @@ +================= +check-future-tags +================= + +Features and usage +================== + +This command detects orphan "FUTURE" tags. + + +Rationale and principles +------------------------ + +Developers sometimes plan broad modifications that will span multiple +files. Instead of littering FIXME annotations everywhere, they can set +a single FIXME annotation and a FUTURE-xxx tag on the same line. Then, +wherever we need to make a modification, we only mention this +FUTURE-xxx tag without any FIXME. If we have to "postpone" a FIXME, +there is only line to touch. + +Example: + +.. code-block:: text + + # in file1.py: + # + # FIXME (jsmith, FUTURE-SWITCH-TO-V2): remove this field when we switch to v2 + # + # in file2.py: + # + # FUTURE-SWITCH-TO-V2: deprecate usage when we switch to v2 + +If we ever remove the FIXME but keep the FUTURE-SWITCH-TO-V2 tag in +``file2.py``, it is a mistake and **check-future-tags** warns us. + + +Usage and possible customization +-------------------------------- + +**check-future-tags** looks for tags that start with ``FUTURE-`` +(e.g. ``FUTURE-SWITCH-TO-V2``) to make sure that at least one of them +appears on the same line as an annotation. If not, it is considered an +orphan tag and is reported as an error. + +As for annotations, you can ignore a line by using +``no-check-fixmes``, and ignore whole files with the :ref:`whitelist +option `. You can configure how tags are detected with +the :ref:`future_tag_regex option `. + +See the :ref:`check_future_tags_configuration` section below for full details. + + +.. _check_future_tags_configuration: + +Configuration +============= + +**check-future-tags** takes its configuration from a TOML file. By +default and if present, ``pyproject.toml`` is read (as a courtesy for +Python projects, even though **check-future-tags** is +language-agnostic). A limited list of options can be overridden via +command line arguments (that you can list with ``check-future-tags +--help``). Such overrides take precedence over the values defined in +the configuration files (or the default values if omitted). + +The TOML configuration file should have a ``[tool.check-future-tags]`` +section, like this: + +.. code-block:: toml + + [tool.check-fixmes] + path = "src" + max-age = 30 + +For an example configuration file, see `the configuration file +`_ +of the **check-future-tags** project itself. + +Here is the list of all options that can be configured via the TOML +configuration file: + +.. contents:: + :local: + :depth: 2 + + +Input options +------------- + +.. _check_future_tags_conf_path: + +``path`` (overridable via the command line) +........................................... + +The path of the directory in which **check-future-tags** looks for +annotations (recursively). It must be a Git checkout repository. + +| Type: string. +| Default: ``"."`` (current working directory). +| Example: ``path = "src"``. + + +.. _check_future_tags_conf_whitelist: + +``whitelist`` +............. + +If the ``no-check-fixmes`` pragma is not appropriate, you may +whitelist whole files by providing a list of glob patterns. + +| Type: list. +| Default: ``[]`` (no whitelist). +| Example: ``whitelist = ["docs/*"]``. + + +Output options +-------------- + +.. _check_future_tags_conf_colorize_errors: + +``colorize-errors`` +................... + +By default, errors (old annotations and orphan FUTURE tags) appear +in red. Set this option to ``false`` if you want to use the +default foreground color. + +| Type: boolean. +| Default: ``true``. +| Example: ``colorize-errors = false``. + + +.. _check_future_tags_conf_xunit_file: + +``xunit-file`` (overridable via the command line) +................................................. + +The path to the xUnit report file to generate. **check-future-tags** +gracefully creates parent directories of the file if they do not +exist. + +| Type: string (a path). +| Default: none (no xUnit file is generated). +| Example: ``xunit-file = "reports/xunit.xml"``. + + +Detection options +----------------- + +.. _check_future_tags_conf_annotations: + +``annotations`` +............... + +The list of annotations to look for. Note that **check-future-tags** is +case insensitive: by default, both "todo", "TODO", "fixme" and +"FIXME" will be reported. + +| Type: list. +| Default: ``["fixme", "todo"]`` (case insensitive). +| Example: ``annotations = ["todo", "optimize", "fixme", "hack"]``. + + +.. _check_future_tags_conf_future_tag_regex: + +``future-tag-regex`` +.................... + +The extended regular expression to use to detect FUTURE tags. + +| Type: string (an extended regular expression). +| Default: ``"FUTURE-[-[:alnum:]\._]+?"``. +| Example: ``future-tag-regex = "HEREAFTER-[-[:alnum:]\._]+?"``. + +.. _check_future_tags_conf_ignored_orphans_annotations: + +``ignored_orphans_annotations`` +............................... + +The list of annotations which will not trigger orphan FUTURE tags +checks. Note that **check-future-tags** is case insensitive: by +default, both "wontfix", "WONTFIX" will be ignored. + +| Type: list. +| Default: ``["wontfix", "xxx"]`` (case insensitive). +| Example: ``ignored_orphans_annotations = ["wontfix", "nofix"]``. diff --git a/docs/forget_me_not.rst b/docs/forget_me_not.rst index b3dad09..5d132e9 100644 --- a/docs/forget_me_not.rst +++ b/docs/forget_me_not.rst @@ -6,10 +6,10 @@ forget-me-not Rationale ========= -**check-fixmes** and **check-branches** can be run as part of the test -suite of each project (by a continuous integration system such as -Jenkins). They break builds when they detect old annotations or -branches. +**check-branches**, **check-fixmes** and **check-future-tags** can be +run as part of the test suite of each project (by a continuous +integration system such as Jenkins). They break builds when they +detect old annotations or branches. But nobody wants to see builds break unexpectedly. What if you were warned that a build *will soon* break because of an old annotation or @@ -47,8 +47,8 @@ details. To detect old annotations and branches, **forget-me-not** uses the configuration files **of each project** (or defaults for project that do not have configuration files). See previous chapters for further -details about the configuration of **check-fixmes** and -**check-branches**. +details about the configuration of **check-branches**, +**check-fixmes** and **check-future-tags**. .. _forget_me_not_configuration: @@ -254,7 +254,7 @@ The user to use when contacting the SMTP host to send e-mail reports. | Example: ``smtp.user = "USERNAME"``. ``smtp.password`` -............. +................. The password to use when contacting the SMTP host to send e-mail reports. | Type: string. diff --git a/docs/index.rst b/docs/index.rst index 9ac91d5..105507a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,8 +4,7 @@ check-oldies **check-oldies** is a collection of programs that warn about old things in code: -- **check-fixmes** warns about old FIXME or TODO annotations and - orphan FUTURE tags. +- **check-fixmes** warns about old FIXME or TODO annotations. If we did not regularly check, we would forget about that FIXME note we wrote a few months ago. **check-fixmes** warns us about it. It is @@ -13,21 +12,23 @@ things in code: not worth to fix, or because it is not relevant anymore), or postpone it. - FUTURE tags: We sometimes plan a broad modification that will span - multiple files. Instead of littering FIXME annotations everywhere, - we set a single FIXME annotation and a FUTURE-xxx tag on the same - line. Then, wherever we need to make a modification, we only - mention this FUTURE-xxx tag without any FIXME. If we ever remove the - FIXME but keep a FUTURE-xxx tag somewhere, it is a mistake and this - tool warns us. +- **check-future-tags** warns about orphan FUTURE tags. + + We sometimes plan a broad modification that will span multiple + files. Instead of littering FIXME annotations everywhere, we set a + single FIXME annotation and a FUTURE-xxxx tag on the same line + (e.g. "FUTURE-MIGRATION-TO-API-V3". Then, wherever we need to make a + modification, we only mention this FUTURE-xxxx tag without any + FIXME. If we ever remove the FIXME but keep a FUTURE-xxxx tag + somewhere, it is a mistake and this tool warns us. - **check-branches** warns about old branches, surprisingly. -- **forget-me-not** runs both programs above on a set of Git +- **forget-me-not** runs all programs above on a set of Git repositories and sends warning e-mails to authors of soon-to-be-old annotations or branches. -In other words: **check-fixmes** and **check-branches** can be run as +In other words: **check-branches**, **check-fixmes** and **check-future-tags** can be run as part of the test suite of each project (by a continuous integration system such as Jenkins). They break builds when they detect old things. On the other hand, **forget-me-not** can be run once a week @@ -51,6 +52,13 @@ Example output: NOK: Some branches are too old. john.smith@example.com - 92 days - jsmith/fix_frobs (https://github.com/Polyconseil/check-oldies/tree/jsmith/fix_frobs), linked to open PR/MR #1 (https://github.com/Polyconseil/check-oldies/pull/1) +.. code-block:: console + + $ check-future-tags + NOK: There are orphan FUTURE tags. + john.smith@example.com - ORPHAN - src/check_oldies/check_fixmes.py:92: Unknown tag FUTURE-NEW-FORMAT-ARGUMENT + + **check-oldies** is written in Python but is language-agnostic. It works on Git repositories but could be extended to other version control systems. It integrates with GitHub and GitLab but can do without it, and @@ -69,6 +77,7 @@ could be extended to work with other code hosting platforms. installation.rst check_fixmes.rst check_branches.rst + check_future_tags.rst forget_me_not.rst contributing.rst changes.rst diff --git a/pyproject.toml b/pyproject.toml index 66b561f..675b0eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,17 @@ whitelist = [ "docs/*.rst", ] +[tool.check-future-tags] +path = "." + +annotations = [ + "fixme", + "todo", +] +whitelist = [ + "docs/*.rst", +] + [tool.isort] multi_line_output = 3 diff --git a/setup.cfg b/setup.cfg index 7547d9d..7ded38b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,8 +37,9 @@ packages=find: [options.entry_points] console-scripts = - check-fixmes = check_oldies.check_fixmes:main check-branches = check_oldies.check_branches:main + check-fixmes = check_oldies.check_fixmes:main + check-future-tags = check_oldies.check_future_tags:main forget-me-not = check_oldies.forget_me_not:main [options.extras_require] diff --git a/src/check_oldies/check_fixmes.py b/src/check_oldies/check_fixmes.py index 497889a..f26a7ed 100755 --- a/src/check_oldies/check_fixmes.py +++ b/src/check_oldies/check_fixmes.py @@ -89,22 +89,14 @@ def main(): uncolorized_out.append(line if annotation.is_old else line) has_old_annotations = any(ann for ann in all_annotations if ann.is_old) - # Look for orphan FUTURE tags - orphan_futures = annotations.get_orphan_futures(config) - for orphan in orphan_futures: - out.append(warn(orphan_str(orphan))) - uncolorized_out.append(orphan_str(orphan)) - out = os.linesep.join(out) - if has_old_annotations or orphan_futures: - err_msg = "NOK: Some annotations are too old, or there are orphan FUTURE tags." + if has_old_annotations: + err_msg = "NOK: Some annotations are too old." print(err_msg) else: err_msg = "" - if all_annotations: - print("OK: All annotations are fresh.") - else: - print("OK: No annotations were found.") + print("OK: All annotations are fresh.") + if out: print(out) @@ -120,7 +112,7 @@ def main(): stderr="", ) - sys.exit(os.EX_DATAERR if has_old_annotations or orphan_futures else os.EX_OK) + sys.exit(os.EX_DATAERR if has_old_annotations else os.EX_OK) if __name__ == "__main__": # pragma: no cover diff --git a/src/check_oldies/check_future_tags.py b/src/check_oldies/check_future_tags.py new file mode 100644 index 0000000..b4fec2a --- /dev/null +++ b/src/check_oldies/check_future_tags.py @@ -0,0 +1,98 @@ +import argparse +import os +import sys + +from . import annotations +from . import configuration +from . import xunit + + +def orphan_str(orphan): + return ( + f"{orphan.author: <15} - ORPHAN - " + f"{orphan.path}:{orphan.line_no}: Unknown tag {orphan.tag}" + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + prog="check-future-tags", description="Check your code for unattended future tags" + ) + parser.add_argument( + "--conf", + help=( + f"Path of the configuration file. " + f"Defaults to {configuration.PYPROJECT_FILENAME} if it exists." + ), + ) + parser.add_argument( + "path", + nargs="?", + help=( + "Git-managed path where search should happen. " + "Defaults to the working directory." + ), + ) + parser.add_argument( + "--no-color", + action="store_false", + default=True, + dest="colorize_errors", + help="Do not colorize errors. Defaults to colorizing errors in red.", + ) + parser.add_argument( + "--xunit-file", + action="store", + help="Path of the xUnit report file to write. Defaults to no xUnit output.", + ) + return parser + + +def main(): + parser = get_parser() + config = configuration.get_config( + "check-future-tags", parser, sys.argv[1:], annotations.Config + ) + if not configuration.is_git_directory(config.path): + sys.exit(f'Invalid path: "{config.path}" is not a Git repository.') + + if config.colorize_errors: + warn = "\033[91m{}\033[0m".format + else: + warn = lambda text: text # pylint: disable=unnecessary-lambda-assignment + + # Look for orphan FUTURE tags + out = [] + uncolorized_out = [] + orphan_futures = annotations.get_orphan_futures(config) + for orphan in orphan_futures: + out.append(warn(orphan_str(orphan))) + uncolorized_out.append(orphan_str(orphan)) + + out = os.linesep.join(out) + if orphan_futures: + err_msg = "NOK: There are orphan FUTURE tags." + print(err_msg) + else: + err_msg = "" + print("OK: No orphan FUTURE tags were found.") + if out: + print(out) + + if config.xunit_file: + uncolorized_out = os.linesep.join(uncolorized_out) + xunit.create_xunit_file( + os.path.abspath(config.xunit_file), + suite_name="check-future-tags", + case_name="future-tags", + class_name="CheckFutureTags", + err_msg=err_msg, + stdout=uncolorized_out, + stderr="", + ) + + sys.exit(os.EX_DATAERR if orphan_futures else os.EX_OK) + + +if __name__ == "__main__": # pragma: no cover + main()