mirror of https://github.com/mongodb/mongo
291 lines
11 KiB
Python
291 lines
11 KiB
Python
#!/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
|
|
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
|
*
|
|
* 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 and "modules/atlas" 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 or "modules/atlas" 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",
|
|
"src/mongo/util/varint_details.cpp",
|
|
"src/mongo/util/varint_details.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())
|