mongo/buildscripts/todo_linter.py
Daniel Moody 76d716abef SERVER-123002 auto revert bot skips todo failures (#50770)
GitOrigin-RevId: 8f77f465ff0400882c85695bd575f8b54cd7512a
2026-03-30 18:33:48 +00:00

189 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""Linter that fails if any TODO comments not referencing SERVER tickets are found.
Matches patterns like:
// TODO(SERVER-XXXXX): fix this
# TODO(SERVER-XXXXX): fix this
// TODO SERVER-XXXXX - fix this
// TODO: fix this
"""
import argparse
import logging
import os
import re
import sys
# Get relative imports to work when the package is not installed on the PYTHONPATH.
if __name__ == "__main__" and __package__ is None:
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__)))))
from buildscripts.linter import git, parallel
from evergreen import RetryingEvergreenApi
EXCLUSION_VALUE = "(Ignore linting)"
TODO_REGEX = re.compile(r'[^"]TODO[^"]')
JIRA_TICKET_REGEX = re.compile(r"\w+-\d+")
FILES_RE = re.compile(r"\.(cpp|c|h|hpp|py|js|mjs|inl|idl|yml|bazel)$")
EVG_CONFIG_FILE = "./.evergreen.yml"
AUTO_REVERT_APP_BOT_MARKER = "auto-revert-app[bot]"
GITHUB_PULL_REQUEST_IDENTIFIERS = {"github_pr", "github_pull_request"}
def is_interesting_file(file_name: str) -> bool:
"""Return true if this file should be checked."""
return (
not file_name.startswith("src/third_party/")
and not file_name.startswith("src/mongo/gotools/")
and not file_name.startswith("src/streams/third_party")
and not file_name.startswith("src/mongo/db/modules/enterprise/src/streams/third_party")
# Exclude these two files because they are filled with TODO SERVER-XXXXX patterns
and not file_name == "buildscripts/todo_linter.py"
and not file_name == "buildscripts/todo_check.py"
and FILES_RE.search(file_name) is not None
)
def check_file(file_name: str) -> bool:
"""Check a single file for TODO comments without any SERVER ticket attached. Returns True if no violations."""
try:
with open(file_name, encoding="utf-8") as f:
lines = f.readlines()
except (OSError, UnicodeDecodeError):
return True
errors: list[tuple[int, str]] = []
for lineno, line in enumerate(lines, 1):
if EXCLUSION_VALUE in line:
continue
if TODO_REGEX.search(line) and not JIRA_TICKET_REGEX.search(line):
# The regex found a TODO without any SERVER ticket present next to it
errors.append((lineno, line.rstrip()))
for lineno, line in errors:
print(
f"Error: {file_name}:{lineno} - todo/server_ticket"
f" - Found TODO with unlinked SERVER ticket reference, make sure to add a valid ticket"
f' to track its cleanup or add "(Ignore linting)" to the line to silence the linter: {line.strip()}'
)
return len(errors) == 0
def get_patch_description(version_id: str) -> str:
"""Return the Evergreen patch description for the given version."""
evg_api = RetryingEvergreenApi.get_api(config_file=EVG_CONFIG_FILE)
version = evg_api.version_by_id(version_id)
return version.message or ""
def should_ignore_todo_lint_failure() -> bool:
"""Return whether TODO lint failures should be ignored for this run."""
requester = os.environ.get("requester") or os.environ.get("REQUESTER")
evergreen_user = os.environ.get("author") or os.environ.get("AUTHOR")
if not any(
identifier in GITHUB_PULL_REQUEST_IDENTIFIERS
for identifier in (requester, evergreen_user)
if identifier
):
return False
version_id = os.environ.get("version_id") or os.environ.get("VERSION_ID")
if not version_id:
return False
try:
patch_description = get_patch_description(version_id)
except Exception as exc: # pylint: disable=broad-except
logging.warning("Unable to determine patch description for TODO lint skip: %s", exc)
return False
# The auto-revert marker is attached to Evergreen's patch description, so use the version
# message rather than trying to infer it from GitHub PR metadata.
return AUTO_REVERT_APP_BOT_MARKER in patch_description.lower()
def _lint_files(file_names: list[str]) -> None:
if not parallel.parallel_process([os.path.abspath(f) for f in file_names], check_file):
if should_ignore_todo_lint_failure():
print(
"Skipping TODO lint failure because this Evergreen GitHub pull request was "
"created by auto-revert-app[bot]."
)
return
print(
"ERROR: Found TODO comments referencing unlinked SERVER tickets."
" Please resolve or remove them before committing."
)
sys.exit(1)
def lint(args) -> None:
"""Lint only Git-tracked files."""
file_names = args.file_names
all_file_names = git.get_files_to_check(file_names, is_interesting_file)
_lint_files(all_file_names)
def lint_all(args) -> None:
"""Lint all files in the working tree."""
all_file_names = git.get_files_to_check_working_tree(is_interesting_file)
_lint_files(all_file_names)
def lint_patch(args) -> None:
"""Lint all files that are divergent from the most recent fork point off of the main branch"""
repo = git.Repo(git.get_base_dir())
if repo.does_branch_exist(args.origin_branch):
origin_branch = args.origin_branch
else:
# We're running this against a stacked PR potentially. Make sure we only test against the parent branch.
origin_branch = repo.get_branch_name()
files = git.get_my_files_to_check(is_interesting_file, origin_branch)
files = [f for f in files if os.path.exists(f)]
_lint_files(files)
def main() -> None:
"""Execute main entry point."""
os.chdir(os.environ.get("BUILD_WORKSPACE_DIRECTORY", "."))
parser = argparse.ArgumentParser(
description="MongoDB TODO SERVER ticket linter. Fails if any unlinked TODO comments are found."
)
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
sub = parser.add_subparsers(title="Linter subcommands", help="sub-command help")
parser_lint = sub.add_parser("lint", help="Lint only Git-tracked files")
parser_lint.add_argument("file_names", nargs="*", help="Globs of files to check")
parser_lint.set_defaults(func=lint)
parser_lint_all = sub.add_parser("lint-all", help="Lint all files in the working tree")
parser_lint_all.set_defaults(func=lint_all)
parser_lint_patch = sub.add_parser(
"lint-patch",
help="Lint files that are different from the most recent fork point from master",
)
parser_lint_patch.add_argument(
"--branch", dest="origin_branch", default="origin/master", help="Branch to compare against"
)
parser_lint_patch.set_defaults(func=lint_patch)
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
if not hasattr(args, "func"):
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == "__main__":
main()