#!/usr/bin/env python3 """Simple C++ Linter.""" import argparse import bisect import io import logging import re import sys _RE_LINT = re.compile("//.*NOLINT") _RE_COMMENT_STRIP = re.compile("//.*") _RE_GENERIC_FCV_COMMENT = re.compile(r"\(Generic FCV reference\):") GENERIC_FCV = [ r"::kLatest", r"::kLastContinuous", r"::kLastLTS", r"::kUpgradingFromLastLTSToLatest", r"::kUpgradingFromLastContinuousToLatest", r"::kDowngradingFromLatestToLastLTS", r"::kDowngradingFromLatestToLastContinuous", r"\.isUpgradingOrDowngrading", r"->isUpgradingOrDowngrading", r"::kDowngradingFromLatestToLastContinuous", r"::kUpgradingFromLastLTSToLastContinuous", ] _RE_GENERIC_FCV_REF = re.compile(r"(" + "|".join(GENERIC_FCV) + r")\b") _RE_FEATURE_FLAG_IGNORE_FCV_CHECK_REF = re.compile(r"isEnabledAndIgnoreFCVUnsafe\(\)") _RE_FEATURE_FLAG_IGNORE_FCV_CHECK_COMMENT = re.compile(r"\(Ignore FCV check\)") _RE_HEADER = re.compile(r"\.(h|hpp)$") class Linter: """Simple C++ Linter.""" _license_header = """\ /** * Copyright (C) {year}-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */""".splitlines() def __init__(self, file_name, raw_lines): """Create new linter.""" self.file_name = file_name self.raw_lines = raw_lines self.clean_lines = [] self.nolint_suppression = [] self.generic_fcv_comments = [] self.feature_flag_ignore_fcv_check_comments = [] self._error_count = 0 def lint(self): """Run linter, returning error count.""" # steps: # - Check for header # - Check for NOLINT and Strip multi line comments # - Run file-level checks # - Run per-line checks start_line = self._check_for_server_side_public_license() self._check_newlines() self._check_and_strip_comments() # Line-level checks for linenum in range(start_line, len(self.clean_lines)): if not self.clean_lines[linenum]: continue # Relax the rule of commenting generic FCV references for files directly related to FCV # implementations. if "feature_compatibility_version" not in self.file_name: self._check_for_generic_fcv(linenum) # Don't check feature_flag.h/cpp where the function is defined and test files. if "feature_flag" not in self.file_name and "test" not in self.file_name: self._check_for_feature_flag_ignore_fcv(linenum) return self._error_count def _check_newlines(self): """Check that each source file ends with a newline character.""" if self.raw_lines and self.raw_lines[-1][-1:] != "\n": self._error( len(self.raw_lines), "mongo/final_newline", "Files must end with a newline character.", ) def _check_and_strip_comments(self): in_multi_line_comment = False for linenum, clean_line in enumerate(self.raw_lines): # Users can write NOLINT different ways # // NOLINT # // Some explanation NOLINT # so we need a regular expression if _RE_LINT.search(clean_line): self.nolint_suppression.append(linenum) if _RE_GENERIC_FCV_COMMENT.search(clean_line): self.generic_fcv_comments.append(linenum) if _RE_FEATURE_FLAG_IGNORE_FCV_CHECK_COMMENT.search(clean_line): self.feature_flag_ignore_fcv_check_comments.append(linenum) if not in_multi_line_comment: if "/*" in clean_line and "*/" not in clean_line: in_multi_line_comment = True clean_line = "" # Trim comments - approximately # Note, this does not understand if // is in a string # i.e. it will think URLs are also comments but this should be good enough to find # violators of the coding convention if "//" in clean_line: clean_line = _RE_COMMENT_STRIP.sub("", clean_line) else: if "*/" in clean_line: in_multi_line_comment = False clean_line = "" self.clean_lines.append(clean_line) def _license_error(self, linenum, msg, category="legal/license"): style_url = "https://github.com/mongodb/mongo/wiki/Server-Code-Style" self._error(linenum, category, "{} See {}".format(msg, style_url)) return (False, linenum) def _check_for_server_side_public_license(self): """Return the number of the line at which the check ended.""" src_iter = (x.rstrip() for x in self.raw_lines) linenum = 0 for linenum, lic_line in enumerate(self._license_header): src_line = next(src_iter, None) if src_line is None: self._license_error(linenum, "Missing or incomplete license header.") return linenum lic_re = re.escape(lic_line).replace(r"\{year\}", r"\d{4}") if not re.fullmatch(lic_re, src_line): self._license_error( linenum, "Incorrect license header.\n" ' Expected: "{}"\n' ' Received: "{}"\n'.format( lic_line, src_line ), ) return linenum # Warn if SSPL appears in Enterprise code, which has a different license. expect_sspl_license = "enterprise" not in self.file_name if not expect_sspl_license: self._license_error( linenum, "Incorrect license header found. Expected Enterprise license.", category="legal/enterprise_license", ) return linenum return linenum def _check_for_generic_fcv(self, linenum): line = self.clean_lines[linenum] if _RE_GENERIC_FCV_REF.search(line): # Find the first generic FCV comment preceding the current line. i = bisect.bisect_right(self.generic_fcv_comments, linenum) if not i or self.generic_fcv_comments[i - 1] < (linenum - 10): self._error( linenum, "mongodb/fcv", 'Please add a comment containing "(Generic FCV reference):" within 10 lines ' + "before the generic FCV reference.", ) def _check_for_feature_flag_ignore_fcv(self, linenum): line = self.clean_lines[linenum] if _RE_FEATURE_FLAG_IGNORE_FCV_CHECK_REF.search(line): # Find the first ignore FCV check comment preceding the current line. i = bisect.bisect_right(self.feature_flag_ignore_fcv_check_comments, linenum) if not i or self.feature_flag_ignore_fcv_check_comments[i - 1] < (linenum - 10): self._error( linenum, "mongodb/fcv", 'Please add a comment containing "(Ignore FCV check)":" within 10 lines ' + "before the isEnabledAndIgnoreFCVUnsafe() function call explaining why " + "the FCV check is ignored.", ) def _error(self, linenum, category, message): if linenum in self.nolint_suppression: return norm_file_name = self.file_name.replace("\\", "/") # Custom clang-tidy check tests purposefully produce errors for # tests to find. They should be ignored. if "mongo_tidy_checks/tests/" in norm_file_name: return if category == "legal/license": # Enterprise module does not have the SSPL license if "enterprise" in self.file_name: return # The following files are in the src/mongo/ directory but technically belong # in src/third_party/ because their copyright does not belong to MongoDB. files_to_ignore = set( [ "src/mongo/scripting/mozjs/PosixNSPR.cpp", "src/mongo/shell/linenoise.cpp", "src/mongo/shell/linenoise.h", "src/mongo/shell/mk_wcwidth.cpp", "src/mongo/shell/mk_wcwidth.h", "src/mongo/util/net/ssl_stream.cpp", "src/mongo/util/scopeguard.h", ] ) for file_to_ignore in files_to_ignore: if file_to_ignore in norm_file_name: return # We count internally from 0 but users count from 1 for line numbers print("Error: %s:%d - %s - %s" % (self.file_name, linenum + 1, category, message)) self._error_count += 1 def lint_file(file_name): """Lint file and print errors to console.""" with io.open(file_name, encoding="utf-8") as file_stream: raw_lines = file_stream.readlines() linter = Linter(file_name, raw_lines) return linter.lint() def main(): # type: () -> int """Execute Main Entry point.""" parser = argparse.ArgumentParser(description="MongoDB Simple C++ Linter.") parser.add_argument("file", type=str, help="C++ input file") parser.add_argument("-v", "--verbose", action="count", help="Enable verbose tracing") args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) try: error_count = lint_file(args.file) if error_count != 0: print('File "{}" failed with {} errors.'.format(args.file, error_count)) return 1 return 0 except Exception as ex: print('Exception while checking file "{}": {}'.format(args.file, ex)) return 2 if __name__ == "__main__": sys.exit(main())