summaryrefslogtreecommitdiff
path: root/buildscripts/linter
diff options
context:
space:
mode:
authorMark Benvenuto <mark.benvenuto@mongodb.com>2019-10-25 19:51:50 +0000
committerevergreen <evergreen@mongodb.com>2019-10-25 19:51:50 +0000
commita54df9e45d85a670bc4dc40339d76347865fab69 (patch)
treed430ebf24bba4c591cb6dfda6286ea4e327e30b2 /buildscripts/linter
parent8fc28f0773ca1efb0a43cc5590b9ca8b9e50559e (diff)
downloadmongo-a54df9e45d85a670bc4dc40339d76347865fab69.tar.gz
SERVER-43936 Implement simpler, quicker python based C++ linter
Diffstat (limited to 'buildscripts/linter')
-rw-r--r--buildscripts/linter/simplecpplint.py311
1 files changed, 311 insertions, 0 deletions
diff --git a/buildscripts/linter/simplecpplint.py b/buildscripts/linter/simplecpplint.py
new file mode 100644
index 00000000000..e1aef8e1e7c
--- /dev/null
+++ b/buildscripts/linter/simplecpplint.py
@@ -0,0 +1,311 @@
+#!/usr/bin/env python3
+"""Simple C++ Linter."""
+
+import argparse
+import io
+import logging
+import re
+import sys
+
+
+def _make_polyfill_regex():
+ polyfill_required_names = [
+ '_',
+ 'adopt_lock',
+ 'async',
+ 'chrono',
+ 'condition_variable',
+ 'condition_variable_any',
+ 'cv_status',
+ 'defer_lock',
+ 'future',
+ 'future_status',
+ 'get_terminate',
+ 'launch',
+ 'lock_guard',
+ 'mutex',
+ 'notify_all_at_thread_exit',
+ 'packaged_task',
+ 'promise',
+ 'recursive_mutex',
+ 'set_terminate',
+ 'shared_lock',
+ 'shared_mutex',
+ 'shared_timed_mutex',
+ 'this_thread(?!::at_thread_exit)',
+ 'thread',
+ 'timed_mutex',
+ 'try_to_lock',
+ 'unique_lock',
+ 'unordered_map',
+ 'unordered_multimap',
+ 'unordered_multiset',
+ 'unordered_set',
+ ]
+
+ qualified_names = ['boost::' + name + "\\b" for name in polyfill_required_names]
+ qualified_names.extend('std::' + name + "\\b" for name in polyfill_required_names)
+ qualified_names_regex = '|'.join(qualified_names)
+ return re.compile('(' + qualified_names_regex + ')')
+
+
+_RE_LINT = re.compile("//.*NOLINT")
+_RE_COMMENT_STRIP = re.compile("//.*")
+
+_RE_PATTERN_MONGO_POLYFILL = _make_polyfill_regex()
+_RE_VOLATILE = re.compile('[^_]volatile')
+_RE_MUTEX = re.compile('[ ({,]stdx?::mutex[ ({]')
+_RE_ASSERT = re.compile(r'\bassert\s*\(')
+
+
+class Linter:
+ """Simple C++ Linter."""
+
+ 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_supression = []
+ self._error_count = 0
+
+ self.found_config_header = False
+
+ def lint(self):
+ """Run linter."""
+ # 3 steps:
+ # 1. Check for header
+ # 2. Check for NOLINT and Strip multi line comments
+ # 3. Run per line checks
+
+ # The license header is 28 lines and we are 0 indexed unless the file does not have the
+ # correct header.
+ start_line = 0
+
+ # We expect the SSPL license in all files but it is not expected in the Enterprise code
+ expect_sspl_license = "enterprise" not in self.file_name
+ has_sspl_license = self._check_for_server_side_public_license(3)
+
+ if has_sspl_license:
+ start_line = 27
+
+ if not expect_sspl_license:
+ self._error(
+ 0, 'legal/enterprise_license', 'Incorrect license header found. '
+ 'Expected Enterprise license. '
+ 'See https://github.com/mongodb/mongo/wiki/Server-Code-Style')
+
+ self._check_and_strip_comments()
+
+ for linenum in range(start_line, len(self.clean_lines)):
+ if not self.clean_lines[linenum]:
+ continue
+
+ self._check_for_mongo_volatile(linenum)
+ self._check_for_mongo_polyfill(linenum)
+ self._check_for_mongo_atomic(linenum)
+ self._check_for_mongo_mutex(linenum)
+ self._check_for_nonmongo_assert(linenum)
+ self._check_for_mongo_config_header(linenum)
+
+ return self._error_count
+
+ def _check_and_strip_comments(self):
+ in_multi_line_comment = False
+
+ for linenum in range(len(self.raw_lines)):
+ clean_line = self.raw_lines[linenum]
+
+ # Users can write NOLINT different ways
+ # // NOLINT
+ # // Some explanation NOLINT
+ # so we need a regular expression
+ if _RE_LINT.search(clean_line):
+ self.nolint_supression.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 _check_for_mongo_volatile(self, linenum):
+ line = self.clean_lines[linenum]
+ if _RE_VOLATILE.search(line) and not "__asm__" in line:
+ self._error(
+ linenum, 'mongodb/volatile',
+ 'Illegal use of the volatile storage keyword, use AtomicWord instead '
+ 'from "mongo/platform/atomic_word.h"')
+
+ def _check_for_mongo_polyfill(self, linenum):
+ line = self.clean_lines[linenum]
+ match = _RE_PATTERN_MONGO_POLYFILL.search(line)
+ if match:
+ self._error(
+ linenum, 'mongodb/polyfill',
+ 'Illegal use of banned name from std::/boost:: for "%s", use mongo::stdx:: variant instead'
+ % (match.group(0)))
+
+ def _check_for_mongo_atomic(self, linenum):
+ line = self.clean_lines[linenum]
+ if 'std::atomic' in line:
+ self._error(
+ linenum, 'mongodb/stdatomic',
+ 'Illegal use of prohibited std::atomic<T>, use AtomicWord<T> or other types '
+ 'from "mongo/platform/atomic_word.h"')
+
+ def _check_for_mongo_mutex(self, linenum):
+ line = self.clean_lines[linenum]
+ if _RE_MUTEX.search(line):
+ self._error(
+ linenum, 'mongodb/stdxmutex', 'Illegal use of prohibited stdx::mutex, '
+ 'use mongo::Mutex from mongo/platform/mutex.h instead.')
+
+ def _check_for_nonmongo_assert(self, linenum):
+ line = self.clean_lines[linenum]
+ if _RE_ASSERT.search(line):
+ self._error(
+ linenum, 'mongodb/assert',
+ 'Illegal use of the bare assert function, use a function from assert_utils.h instead.'
+ )
+
+ def _check_for_server_side_public_license(self, copyright_offset):
+ license_header = '''\
+ * 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()
+
+ # We expect the first line of the license header to follow shortly after the
+ # "Copyright" message.
+ if r'This program is free software' in self.raw_lines[copyright_offset]:
+ license_header_start_line = copyright_offset
+ for i in range(len(license_header)): # pylint: disable=consider-using-enumerate
+ line = i + license_header_start_line
+ # We don't trim the lines so we check length and then do strncmp
+ if line >= len(self.raw_lines) or \
+ len(self.raw_lines[line]) != len(license_header[i]) + 1 or \
+ self.raw_lines[line][:len(license_header[i])] != license_header[i]:
+ self._error(
+ 0, 'legal/license', 'Incorrect license header found. '
+ 'Expected "' + license_header[i] + '". '
+ 'See https://github.com/mongodb/mongo/wiki/Server-Code-Style')
+ # We break here to stop reporting legal/license errors for this file.
+ return False
+ else:
+ self._error(
+ 0, 'legal/license', 'No license header found. '
+ 'See https://github.com/mongodb/mongo/wiki/Server-Code-Style')
+ return False
+
+ return True
+
+ def _check_for_mongo_config_header(self, linenum):
+ """Check for a config file."""
+ if self.found_config_header:
+ return
+
+ line = self.clean_lines[linenum]
+ self.found_config_header = line.startswith('#include "mongo/config.h"')
+
+ if not self.found_config_header and "MONGO_CONFIG_" in line:
+ self._error(linenum, 'build/config_h_include',
+ 'MONGO_CONFIG define used without prior inclusion of config.h.')
+
+ def _error(self, linenum, category, message):
+ if linenum in self.nolint_supression:
+ 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/md5.cpp',
+ 'src/mongo/util/md5.h',
+ 'src/mongo/util/md5main.cpp',
+ 'src/mongo/util/net/ssl_stream.cpp',
+ 'src/mongo/util/scopeguard.h',
+ ])
+
+ norm_file_name = self.file_name.replace('\\', '/')
+ 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: () -> None
+ """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)
+
+ if not lint_file(args.file):
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()