From 0bb5bc5557c4592764e215aa11dd41ddb5942acc Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 18 Nov 2025 17:04:50 -0500 Subject: [PATCH] refactor: clint.py => clint.lua Problem: `clint.py` is the last python in our codebase, and beyond that it needs some cleanup. And it lacks tests, so modifying it can be painful. Also, we need a way to add ad-hoc lint rules for *Lua*, so it will help to have our ad-hoc rules for C in the same language (the scripts may share functions/techniques): https://github.com/neovim/neovim/issues/28372 Solution: - convert to `clint.lua` (mostly AI-generated, but it now has test coverage, unlike `clint.py`) - drop rules that are no longer needed: - "readability/multiline_string" - technially still relevant, but very uncommon so doesn't really matter. - "--line-length" - Not used in the old clint.py, nor the new clint.lua. - "comment whitespace" check - It is enforced by uncrustify. - "TODO" check - The `-google-readability-function-size` clang-tidy rule enforces "TODO(user)" format. (It was already enabled long ago.) --- .github/workflows/test.yml | 2 +- runtime/doc/dev_style.txt | 6 +- src/clint.lua | 1584 ++++++++++++++++ src/clint.py | 2414 ------------------------- src/nvim/CMakeLists.txt | 5 +- src/nvim/eval/typval.c | 54 +- src/uncrustify.cfg | 2 +- test/functional/fixtures/clint_test.c | 163 ++ test/functional/script/clint_spec.lua | 50 + 9 files changed, 1832 insertions(+), 2448 deletions(-) create mode 100755 src/clint.lua delete mode 100755 src/clint.py create mode 100644 test/functional/fixtures/clint_test.c create mode 100644 test/functional/script/clint_spec.lua diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a4cdba25e..e4917fc241 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: run: cmake --build build --target lintsh - if: success() || failure() && steps.abort_job.outputs.status == 'success' - name: clint.py + name: clint.lua run: cmake --build build --target lintc-clint - if: success() || failure() && steps.abort_job.outputs.status == 'success' diff --git a/runtime/doc/dev_style.txt b/runtime/doc/dev_style.txt index 608de3bba3..8634a6d65b 100644 --- a/runtime/doc/dev_style.txt +++ b/runtime/doc/dev_style.txt @@ -119,9 +119,9 @@ Nvim-Specific Magic clint ~ -Use `clint.py` to detect style errors. +Use `clint.lua` to detect style errors. -`src/clint.py` is a Python script that reads a source file and identifies +`src/clint.lua` is a Lua script that reads a source file and identifies style errors. It is not perfect, and has both false positives and false negatives, but it is still a valuable tool. False positives can be ignored by putting `// NOLINT` at the end of the line. @@ -129,7 +129,7 @@ putting `// NOLINT` at the end of the line. uncrustify ~ src/uncrustify.cfg is the authority for expected code formatting, for cases -not covered by clint.py. We remove checks in clint.py if they are covered by +not covered by clint.lua. We remove checks in clint.lua if they are covered by uncrustify rules. ============================================================================== diff --git a/src/clint.lua b/src/clint.lua new file mode 100755 index 0000000000..836417ed3d --- /dev/null +++ b/src/clint.lua @@ -0,0 +1,1584 @@ +#!/usr/bin/env nvim -l + +-- Lints C files in the Neovim source tree. +-- Based on Google "cpplint", modified for Neovim. +-- +-- Test coverage: `test/functional/script/clint_spec.lua` +-- +-- This can get very confused by /* and // inside strings! We do a small hack, +-- which is to ignore //'s with "'s after them on the same line, but it is far +-- from perfect (in either direction). + +local vim = vim + +-- Error categories used for filtering +local ERROR_CATEGORIES = { + 'build/endif_comment', + 'build/header_guard', + 'build/include_defs', + 'build/defs_header', + 'build/printf_format', + 'build/storage_class', + 'build/init_macro', + 'readability/bool', + 'readability/multiline_comment', + -- Dropped 'readability/multiline_string' detection because it is too buggy, and uncommon. + -- 'readability/multiline_string', + 'readability/nul', + 'readability/utf8', + 'readability/increment', + 'runtime/arrays', + 'runtime/int', + 'runtime/memset', + 'runtime/printf', + 'runtime/printf_format', + 'runtime/threadsafe_fn', + 'runtime/deprecated', + 'whitespace/indent', + 'whitespace/operators', + 'whitespace/cast', +} + +-- Default filters (empty by default) +local DEFAULT_FILTERS = {} + +-- Assembly state constants +local NO_ASM = 0 -- Outside of inline assembly block +local INSIDE_ASM = 1 -- Inside inline assembly block +local END_ASM = 2 -- Last line of inline assembly block +local BLOCK_ASM = 3 -- The whole block is an inline assembly block + +-- Regex compilation cache +local regexp_compile_cache = {} + +-- Error suppression state +local error_suppressions = {} +local error_suppressions_2 = {} + +-- Configuration +local valid_extensions = { c = true, h = true } + +-- Precompiled regex patterns (only the ones still used) +local RE_SUPPRESSION = vim.regex([[\]]) +local RE_COMMENTLINE = vim.regex([[^\s*//]]) +local RE_PATTERN_INCLUDE = vim.regex([[^\s*#\s*include\s*\([<"]\)\([^>"]*\)[>"].*$]]) + +-- Assembly block matching (using Lua pattern instead of vim.regex for simplicity) +local function match_asm(line) + return line:find('^%s*asm%s*[{(]') + or line:find('^%s*_asm%s*[{(]') + or line:find('^%s*__asm%s*[{(]') + or line:find('^%s*__asm__%s*[{(]') +end + +-- Threading function replacements +local threading_list = { + { 'asctime(', 'os_asctime_r(' }, + { 'ctime(', 'os_ctime_r(' }, + { 'getgrgid(', 'os_getgrgid_r(' }, + { 'getgrnam(', 'os_getgrnam_r(' }, + { 'getlogin(', 'os_getlogin_r(' }, + { 'getpwnam(', 'os_getpwnam_r(' }, + { 'getpwuid(', 'os_getpwuid_r(' }, + { 'gmtime(', 'os_gmtime_r(' }, + { 'localtime(', 'os_localtime_r(' }, + { 'strtok(', 'os_strtok_r(' }, + { 'ttyname(', 'os_ttyname_r(' }, + { 'asctime_r(', 'os_asctime_r(' }, + { 'ctime_r(', 'os_ctime_r(' }, + { 'getgrgid_r(', 'os_getgrgid_r(' }, + { 'getgrnam_r(', 'os_getgrnam_r(' }, + { 'getlogin_r(', 'os_getlogin_r(' }, + { 'getpwnam_r(', 'os_getpwnam_r(' }, + { 'getpwuid_r(', 'os_getpwuid_r(' }, + { 'gmtime_r(', 'os_gmtime_r(' }, + { 'localtime_r(', 'os_localtime_r(' }, + { 'strtok_r(', 'os_strtok_r(' }, + { 'ttyname_r(', 'os_ttyname_r(' }, +} + +-- Memory function replacements +local memory_functions = { + { 'malloc(', 'xmalloc(' }, + { 'calloc(', 'xcalloc(' }, + { 'realloc(', 'xrealloc(' }, + { 'strdup(', 'xstrdup(' }, + { 'free(', 'xfree(' }, +} +local memory_ignore_pattern = vim.regex([[src/nvim/memory.c$]]) + +-- OS function replacements +local os_functions = { + { 'setenv(', 'os_setenv(' }, + { 'getenv(', 'os_getenv(' }, + { '_wputenv(', 'os_setenv(' }, + { '_putenv_s(', 'os_setenv(' }, + { 'putenv(', 'os_setenv(' }, + { 'unsetenv(', 'os_unsetenv(' }, +} + +-- CppLintState class equivalent +local CppLintState = {} +CppLintState.__index = CppLintState + +function CppLintState.new() + local self = setmetatable({}, CppLintState) + self.verbose_level = 1 + self.error_count = 0 + self.filters = vim.deepcopy(DEFAULT_FILTERS) + self.counting = 'total' + self.errors_by_category = {} + self.stdin_filename = '' + self.output_format = 'emacs' + self.record_errors_file = nil + self.suppressed_errors = vim.defaulttable(function() + return vim.defaulttable(function() + return {} + end) + end) + return self +end + +function CppLintState:set_output_format(output_format) + self.output_format = output_format +end + +function CppLintState:set_verbose_level(level) + local last_verbose_level = self.verbose_level + self.verbose_level = level + return last_verbose_level +end + +function CppLintState:set_counting_style(counting_style) + self.counting = counting_style +end + +function CppLintState:set_filters(filters) + self.filters = vim.deepcopy(DEFAULT_FILTERS) + for filt in vim.gsplit(filters, ',', { trimempty = true }) do + local clean_filt = vim.trim(filt) + if clean_filt ~= '' then + table.insert(self.filters, clean_filt) + end + end + + for _, filt in ipairs(self.filters) do + if not (filt:sub(1, 1) == '+' or filt:sub(1, 1) == '-') then + error('Every filter in --filters must start with + or - (' .. filt .. ' does not)') + end + end +end + +function CppLintState:reset_error_counts() + self.error_count = 0 + self.errors_by_category = {} +end + +function CppLintState:increment_error_count(category) + self.error_count = self.error_count + 1 + if self.counting == 'toplevel' or self.counting == 'detailed' then + local cat = category + if self.counting ~= 'detailed' then + cat = category:match('([^/]+)') or category + end + if not self.errors_by_category[cat] then + self.errors_by_category[cat] = 0 + end + self.errors_by_category[cat] = self.errors_by_category[cat] + 1 + end +end + +function CppLintState:print_error_counts() + for category, count in pairs(self.errors_by_category) do + io.write(string.format("Category '%s' errors found: %d\n", category, count)) + end + if self.error_count > 0 then + io.write(string.format('Total errors found: %d\n', self.error_count)) + end +end + +function CppLintState:suppress_errors_from(fname) + if not fname then + return + end + + local ok, content = pcall(vim.fn.readfile, fname) + if not ok then + return + end + + for _, line in ipairs(content) do + local ok2, data = pcall(vim.json.decode, line) + if ok2 then + local fname2, lines, category = data[1], data[2], data[3] + local lines_tuple = vim.tbl_islist(lines) and lines or { lines } + self.suppressed_errors[fname2][vim.inspect(lines_tuple)][category] = true + end + end +end + +function CppLintState:record_errors_to(fname) + if not fname then + return + end + self.record_errors_file = io.open(fname, 'w') +end + +-- Global state instance +local cpplint_state = CppLintState.new() + +-- Utility functions +local function match(pattern, s) + if not regexp_compile_cache[pattern] then + regexp_compile_cache[pattern] = vim.regex(pattern) + end + local s_idx, e_idx = regexp_compile_cache[pattern]:match_str(s) + if s_idx then + local match_obj = {} + match_obj.start = s_idx + match_obj.finish = e_idx + function match_obj.group(n) + if n == 0 then + return s:sub(s_idx + 1, e_idx) + else + -- For subgroups, we need to use a different approach + -- This is a simplified version - full regex groups would need more complex handling + return s:sub(s_idx + 1, e_idx) + end + end + return match_obj + end + return nil +end + +-- NOLINT suppression functions +local function parse_nolint_suppressions(raw_line, linenum) + local s_idx, e_idx = RE_SUPPRESSION:match_str(raw_line) + if not s_idx then + return + end + + -- Extract what comes after NOLINT, looking for optional (category) + local after_nolint = raw_line:sub(e_idx + 1) + local category = after_nolint:match('^%s*(%([^)]*%))') + + if not category or category == '(*)' then + -- Suppress all errors on this line + if not error_suppressions[vim.NIL] then + error_suppressions[vim.NIL] = {} + end + table.insert(error_suppressions[vim.NIL], linenum) + else + -- Extract category name from parentheses + local cat_name = category:match('^%((.-)%)$') + if cat_name then + for _, cat in ipairs(ERROR_CATEGORIES) do + if cat == cat_name then + if not error_suppressions[cat_name] then + error_suppressions[cat_name] = {} + end + table.insert(error_suppressions[cat_name], linenum) + break + end + end + end + end +end + +local function reset_nolint_suppressions() + error_suppressions = {} +end + +local function reset_known_error_suppressions() + error_suppressions_2 = {} +end + +local function is_error_suppressed_by_nolint(category, linenum) + local cat_suppressed = error_suppressions[category] or {} + local all_suppressed = error_suppressions[vim.NIL] or {} + + for _, line in ipairs(cat_suppressed) do + if line == linenum then + return true + end + end + for _, line in ipairs(all_suppressed) do + if line == linenum then + return true + end + end + return false +end + +local function is_error_in_suppressed_errors_list(category, linenum) + local key = category .. ':' .. linenum + return error_suppressions_2[key] == true +end + +-- FileInfo class equivalent +local FileInfo = {} +FileInfo.__index = FileInfo + +function FileInfo.new(filename) + local self = setmetatable({}, FileInfo) + self._filename = filename + return self +end + +function FileInfo:full_name() + local abspath = vim.fn.fnamemodify(self._filename, ':p') + return abspath:gsub('\\', '/') +end + +function FileInfo:relative_path() + local fullname = self:full_name() + + if vim.fn.filereadable(fullname) == 1 then + -- Find git repository root using vim.fs.root + local git_root = vim.fs.root(fullname, '.git') + if git_root then + local root_dir = vim.fs.joinpath(git_root, 'src', 'nvim') + local relpath = vim.fs.relpath(root_dir, fullname) + if relpath then + return relpath + end + end + end + + return fullname +end + +-- Error reporting +local function should_print_error(category, confidence, linenum) + if is_error_suppressed_by_nolint(category, linenum) then + return false + end + if is_error_in_suppressed_errors_list(category, linenum) then + return false + end + if confidence < cpplint_state.verbose_level then + return false + end + + local is_filtered = false + for _, one_filter in ipairs(cpplint_state.filters) do + if one_filter:sub(1, 1) == '-' then + if category:find(one_filter:sub(2), 1, true) == 1 then + is_filtered = true + end + elseif one_filter:sub(1, 1) == '+' then + if category:find(one_filter:sub(2), 1, true) == 1 then + is_filtered = false + end + end + end + + return not is_filtered +end + +local function error_func(filename, linenum, category, confidence, message) + if should_print_error(category, confidence, linenum) then + cpplint_state:increment_error_count(category) + + if cpplint_state.output_format == 'vs7' then + io.write( + string.format('%s(%s): %s [%s] [%d]\n', filename, linenum, message, category, confidence) + ) + elseif cpplint_state.output_format == 'eclipse' then + io.write( + string.format( + '%s:%s: warning: %s [%s] [%d]\n', + filename, + linenum, + message, + category, + confidence + ) + ) + elseif cpplint_state.output_format == 'gh_action' then + io.write( + string.format( + '::error file=%s,line=%s::%s [%s] [%d]\n', + filename, + linenum, + message, + category, + confidence + ) + ) + else + io.write( + string.format('%s:%s: %s [%s] [%d]\n', filename, linenum, message, category, confidence) + ) + end + end +end + +-- String processing functions +local function is_cpp_string(line) + line = line:gsub('\\\\', 'XX') + local quote_count = select(2, line:gsub('"', '')) + local escaped_quote_count = select(2, line:gsub('\\"', '')) + local combined_quote_count = select(2, line:gsub("'\"'", '')) + return ((quote_count - escaped_quote_count - combined_quote_count) % 2) == 1 +end + +local function cleanse_comments(line) + local commentpos = line:find('//') + if commentpos and not is_cpp_string(line:sub(1, commentpos - 1)) then + line = line:sub(1, commentpos - 1):gsub('%s+$', '') + end + -- Remove /* */ comments + line = line:gsub('/%*.-%*/', '') + return line +end + +-- CleansedLines class equivalent +local CleansedLines = {} +CleansedLines.__index = CleansedLines + +function CleansedLines.new(lines, init_lines) + local self = setmetatable({}, CleansedLines) + self.elided = {} + self.lines = {} + self.raw_lines = lines + self._num_lines = #lines + self.init_lines = init_lines + self.lines_without_raw_strings = lines + self.elided_with_space_strings = {} + + for linenum = 1, #self.lines_without_raw_strings do + local line = self.lines_without_raw_strings[linenum] + table.insert(self.lines, cleanse_comments(line)) + + local elided = self:_collapse_strings(line) + table.insert(self.elided, cleanse_comments(elided)) + + local elided_spaces = self:_collapse_strings(line, true) + table.insert(self.elided_with_space_strings, cleanse_comments(elided_spaces)) + end + + return self +end + +function CleansedLines:num_lines() + return self._num_lines +end + +function CleansedLines:_collapse_strings(elided, keep_spaces) + if not RE_PATTERN_INCLUDE:match_str(elided) then + -- Remove escaped characters + elided = elided:gsub('\\[abfnrtv?"\\\'\\]', keep_spaces and ' ' or '') + elided = elided:gsub('\\%d+', keep_spaces and ' ' or '') + elided = elided:gsub('\\x[0-9a-fA-F]+', keep_spaces and ' ' or '') + + if keep_spaces then + elided = elided:gsub("'([^'])'", function(c) + return "'" .. string.rep(' ', #c) .. "'" + end) + elided = elided:gsub('"([^"]*)"', function(c) + return '"' .. string.rep(' ', #c) .. '"' + end) + else + elided = elided:gsub("'([^'])'", "''") + elided = elided:gsub('"([^"]*)"', '""') + end + end + return elided +end + +-- Helper functions for argument parsing +local function print_usage(message) + local usage = [[ +Syntax: clint.lua [--verbose=#] [--output=vs7] [--filter=-x,+y,...] + [--counting=total|toplevel|detailed] [--root=subdir] + [--linelength=digits] [--record-errors=file] + [--suppress-errors=file] [--stdin-filename=filename] + [file] ... + + The style guidelines this tries to follow are those in + https://neovim.io/doc/user/dev_style.html#dev-style + + Note: This is Google's https://github.com/cpplint/cpplint modified for use + with the Neovim project. + + Every problem is given a confidence score from 1-5, with 5 meaning we are + certain of the problem, and 1 meaning it could be a legitimate construct. + This will miss some errors, and is not a substitute for a code review. + + To suppress false-positive errors of a certain category, add a + 'NOLINT(category)' comment to the line. NOLINT or NOLINT(*) + suppresses errors of all categories on that line. + + The files passed in will be linted; at least one file must be provided. + Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the + extensions with the --extensions flag. + + Flags: + + output=vs7 + By default, the output is formatted to ease emacs parsing. Visual Studio + compatible output (vs7) may also be used. Other formats are unsupported. + + verbose=# + Specify a number 0-5 to restrict errors to certain verbosity levels. + + filter=-x,+y,... + Specify a comma-separated list of category-filters to apply: only + error messages whose category names pass the filters will be printed. + (Category names are printed with the message and look like + "[whitespace/indent]".) Filters are evaluated left to right. + "-FOO" and "FOO" means "do not print categories that start with FOO". + "+FOO" means "do print categories that start with FOO". + + Examples: --filter=-whitespace,+whitespace/braces + --filter=whitespace,runtime/printf,+runtime/printf_format + --filter=-,+build/include_what_you_use + + To see a list of all the categories used in cpplint, pass no arg: + --filter= + + counting=total|toplevel|detailed + The total number of errors found is always printed. If + 'toplevel' is provided, then the count of errors in each of + the top-level categories like 'build' and 'whitespace' will + also be printed. If 'detailed' is provided, then a count + is provided for each category. + + root=subdir + The root directory used for deriving header guard CPP variable. + By default, the header guard CPP variable is calculated as the relative + path to the directory that contains .git, .hg, or .svn. When this flag + is specified, the relative path is calculated from the specified + directory. If the specified directory does not exist, this flag is + ignored. + + Examples: + Assuing that src/.git exists, the header guard CPP variables for + src/chrome/browser/ui/browser.h are: + + No flag => CHROME_BROWSER_UI_BROWSER_H_ + --root=chrome => BROWSER_UI_BROWSER_H_ + --root=chrome/browser => UI_BROWSER_H_ + + linelength=digits + This is the allowed line length for the project. The default value is + 80 characters. + + Examples: + --linelength=120 + + extensions=extension,extension,... + The allowed file extensions that cpplint will check + + Examples: + --extensions=hpp,cpp + + record-errors=file + Record errors to the given location. This file may later be used for error + suppression using suppress-errors flag. + + suppress-errors=file + Errors listed in the given file will not be reported. + + stdin-filename=filename + Use specified filename when reading from stdin (file "-"). +]] + + if message then + io.stderr:write(usage .. '\nFATAL ERROR: ' .. message .. '\n') + os.exit(1) + else + io.write(usage) + os.exit(0) + end +end + +local function print_categories() + for _, cat in ipairs(ERROR_CATEGORIES) do + io.write(' ' .. cat .. '\n') + end + os.exit(0) +end + +-- Argument parsing +local function parse_arguments(args) + local filenames = {} + local opts = { + output_format = 'emacs', + verbose_level = 1, + filters = '', + counting_style = 'total', + extensions = { 'c', 'h' }, + record_errors_file = nil, + suppress_errors_file = nil, + stdin_filename = '', + } + + local i = 1 + while i <= #args do + local arg = args[i] + + if arg == '--help' then + print_usage() + elseif arg:sub(1, 9) == '--output=' then + local format = arg:sub(10) + if + format ~= 'emacs' + and format ~= 'vs7' + and format ~= 'eclipse' + and format ~= 'gh_action' + then + print_usage('The only allowed output formats are emacs, vs7 and eclipse.') + end + opts.output_format = format + elseif arg:sub(1, 10) == '--verbose=' then + opts.verbose_level = tonumber(arg:sub(11)) + elseif arg:sub(1, 9) == '--filter=' then + opts.filters = arg:sub(10) + if opts.filters == '' then + print_categories() + end + elseif arg:sub(1, 12) == '--counting=' then + local style = arg:sub(13) + if style ~= 'total' and style ~= 'toplevel' and style ~= 'detailed' then + print_usage('Valid counting options are total, toplevel, and detailed') + end + opts.counting_style = style + elseif arg:sub(1, 13) == '--extensions=' then + local exts = arg:sub(14) + opts.extensions = {} + for ext in vim.gsplit(exts, ',', { trimempty = true }) do + table.insert(opts.extensions, vim.trim(ext)) + end + elseif arg:sub(1, 16) == '--record-errors=' then + opts.record_errors_file = arg:sub(17) + elseif arg:sub(1, 18) == '--suppress-errors=' then + opts.suppress_errors_file = arg:sub(19) + elseif arg:sub(1, 17) == '--stdin-filename=' then + opts.stdin_filename = arg:sub(18) + elseif arg:sub(1, 2) == '--' then + print_usage('Unknown option: ' .. arg) + else + table.insert(filenames, arg) + end + + i = i + 1 + end + + if #filenames == 0 then + print_usage('No files were specified.') + end + + return filenames, opts +end + +-- Lint checking functions + +local function find_next_multiline_comment_start(lines, lineix) + while lineix <= #lines do + if lines[lineix]:find('^%s*/%*') then + if not lines[lineix]:find('%*/', 1, true) then + -- Check if this line ends with backslash (line continuation) + -- If so, don't treat it as a multiline comment start + local line = lines[lineix] + if not line:find('\\%s*$') then + return lineix + end + end + end + lineix = lineix + 1 + end + return #lines + 1 +end + +local function find_next_multiline_comment_end(lines, lineix) + while lineix <= #lines do + if lines[lineix]:find('%*/%s*$') then + return lineix + end + lineix = lineix + 1 + end + return #lines + 1 +end + +local function remove_multiline_comments_from_range(lines, begin, finish) + for i = begin, finish do + lines[i] = '// dummy' + end +end + +local function remove_multiline_comments(filename, lines, error) + local lineix = 1 + while lineix <= #lines do + local lineix_begin = find_next_multiline_comment_start(lines, lineix) + if lineix_begin > #lines then + return + end + local lineix_end = find_next_multiline_comment_end(lines, lineix_begin) + if lineix_end > #lines then + error( + filename, + lineix_begin, + 'readability/multiline_comment', + 5, + 'Could not find end of multi-line comment' + ) + return + end + remove_multiline_comments_from_range(lines, lineix_begin, lineix_end) + lineix = lineix_end + 1 + end +end + +local function check_for_header_guard(filename, lines, error) + if filename:match('%.c%.h$') or FileInfo.new(filename):relative_path() == 'func_attr.h' then + return + end + + local found_pragma = false + for _, line in ipairs(lines) do + if line:find('#pragma%s+once') then + found_pragma = true + break + end + end + + if not found_pragma then + error(filename, 1, 'build/header_guard', 5, 'No "#pragma once" found in header') + end +end + +local function check_includes(filename, lines, error) + if + filename:match('%.c%.h$') + or filename:match('%.in%.h$') + or FileInfo.new(filename):relative_path() == 'func_attr.h' + or FileInfo.new(filename):relative_path() == 'os/pty_proc.h' + then + return + end + + local check_includes_ignore = { + 'src/nvim/api/private/validate.h', + 'src/nvim/assert_defs.h', + 'src/nvim/channel.h', + 'src/nvim/charset.h', + 'src/nvim/eval/typval.h', + 'src/nvim/event/multiqueue.h', + 'src/nvim/garray.h', + 'src/nvim/globals.h', + 'src/nvim/highlight.h', + 'src/nvim/lua/executor.h', + 'src/nvim/main.h', + 'src/nvim/mark.h', + 'src/nvim/msgpack_rpc/channel_defs.h', + 'src/nvim/msgpack_rpc/unpacker.h', + 'src/nvim/option.h', + 'src/nvim/os/pty_conpty_win.h', + 'src/nvim/os/pty_proc_win.h', + } + + local skip_headers = { + 'auto/config.h', + 'klib/klist.h', + 'klib/kvec.h', + 'mpack/mpack_core.h', + 'mpack/object.h', + 'nvim/func_attr.h', + 'termkey/termkey.h', + 'vterm/vterm.h', + 'xdiff/xdiff.h', + } + + for _, ignore in ipairs(check_includes_ignore) do + if filename:match(ignore .. '$') then + return + end + end + + for i, line in ipairs(lines) do + local matched = match('#\\s*include\\s*"([^"]*)"', line) + if matched then + local name = line:match('#\\s*include\\s*"([^"]*)"') + local should_skip = false + for _, skip in ipairs(skip_headers) do + if name == skip then + should_skip = true + break + end + end + + if + not should_skip + and not name:match('%.h%.generated%.h$') + and not name:match('/defs%.h$') + and not name:match('_defs%.h$') + and not name:match('%.h%.inline%.generated%.h$') + and not name:match('_defs%.generated%.h$') + and not name:match('_enum%.generated%.h$') + then + error( + filename, + i - 1, + 'build/include_defs', + 5, + 'Headers should not include non-"_defs" headers' + ) + end + end + end +end + +local function check_non_symbols(filename, lines, error) + for i, line in ipairs(lines) do + if line:match('^EXTERN ') or line:match('^extern ') then + error( + filename, + i - 1, + 'build/defs_header', + 5, + '"_defs" headers should not contain extern variables' + ) + end + end +end + +local function check_for_bad_characters(filename, lines, error) + for linenum, line in ipairs(lines) do + if line:find('\239\187\191') then -- UTF-8 BOM + error( + filename, + linenum - 1, + 'readability/utf8', + 5, + 'Line contains invalid UTF-8 (or Unicode replacement character).' + ) + end + if line:find('\0') then + error(filename, linenum - 1, 'readability/nul', 5, 'Line contains NUL byte.') + end + end +end + +local function check_for_multiline_comments_and_strings(filename, clean_lines, linenum, error) + -- Use elided line (with strings collapsed) to avoid false positives from /* */ in strings + local line = clean_lines.elided[linenum + 1] + if not line then + return + end + + -- Remove all \\ (escaped backslashes) from the line. They are OK, and the + -- second (escaped) slash may trigger later \" detection erroneously. + line = line:gsub('\\\\', '') + + local comment_count = select(2, line:gsub('/%*', '')) + local comment_end_count = select(2, line:gsub('%*/', '')) + -- Only warn if there are actually more opening than closing comments + -- (accounting for the possibility that this is a multi-line comment that continues) + if comment_count > comment_end_count and comment_count > 0 then + error( + filename, + linenum, + 'readability/multiline_comment', + 5, + 'Complex multi-line /*...*/-style comment found. ' + .. 'Lint may give bogus warnings. ' + .. 'Consider replacing these with //-style comments, ' + .. 'with #if 0...#endif, ' + .. 'or with more clearly structured multi-line comments.' + ) + end + + -- Dropped 'readability/multiline_string' detection because it produces too many false positives + -- with escaped quotes in C strings and character literals. +end + +local function check_for_old_style_comments(filename, line, linenum, error) + if line:find('/%*') and line:sub(-1) ~= '\\' and not RE_COMMENTLINE:match_str(line) then + error( + filename, + linenum, + 'readability/old_style_comment', + 5, + '/*-style comment found, it should be replaced with //-style. ' + .. '/*-style comments are only allowed inside macros. ' + .. 'Note that you should not use /*-style comments to document ' + .. 'macros itself, use doxygen-style comments for this.' + ) + end +end + +local function check_posix_threading(filename, clean_lines, linenum, error) + local line = clean_lines.elided[linenum + 1] + + for _, pair in ipairs(threading_list) do + local single_thread_function, multithread_safe_function = pair[1], pair[2] + local start_pos = line:find(single_thread_function, 1, true) + + if start_pos then + local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' + if + start_pos == 1 + or ( + not prev_char:match('%w') + and prev_char ~= '_' + and prev_char ~= '.' + and prev_char ~= '>' + ) + then + error( + filename, + linenum, + 'runtime/threadsafe_fn', + 2, + 'Use ' + .. multithread_safe_function + .. '...) instead of ' + .. single_thread_function + .. '...). If it is missing, consider implementing it;' + .. ' see os_localtime_r for an example.' + ) + end + end + end +end + +local function check_memory_functions(filename, clean_lines, linenum, error) + if memory_ignore_pattern:match_str(filename) then + return + end + + local line = clean_lines.elided[linenum + 1] + + for _, pair in ipairs(memory_functions) do + local func, suggested_func = pair[1], pair[2] + local start_pos = line:find(func, 1, true) + + if start_pos then + local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' + if + start_pos == 1 + or ( + not prev_char:match('%w') + and prev_char ~= '_' + and prev_char ~= '.' + and prev_char ~= '>' + ) + then + error( + filename, + linenum, + 'runtime/memory_fn', + 2, + 'Use ' .. suggested_func .. '...) instead of ' .. func .. '...).' + ) + end + end + end +end + +local function check_os_functions(filename, clean_lines, linenum, error) + local line = clean_lines.elided[linenum + 1] + + for _, pair in ipairs(os_functions) do + local func, suggested_func = pair[1], pair[2] + local start_pos = line:find(func, 1, true) + + if start_pos then + local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' + if + start_pos == 1 + or ( + not prev_char:match('%w') + and prev_char ~= '_' + and prev_char ~= '.' + and prev_char ~= '>' + ) + then + error( + filename, + linenum, + 'runtime/os_fn', + 2, + 'Use ' .. suggested_func .. '...) instead of ' .. func .. '...).' + ) + end + end + end +end + +local function check_language(filename, clean_lines, linenum, error) + local line = clean_lines.elided[linenum + 1] + if not line or line == '' then + return + end + + -- Check for verboten C basic types + local short_regex = vim.regex([[\]]) + local long_long_regex = vim.regex([[\ \+\]]) + + if short_regex:match_str(line) then + error( + filename, + linenum, + 'runtime/int', + 4, + 'Use int16_t/int64_t/etc, rather than the C type short' + ) + elseif long_long_regex:match_str(line) then + error( + filename, + linenum, + 'runtime/int', + 4, + 'Use int16_t/int64_t/etc, rather than the C type long long' + ) + end + + -- Check for snprintf with non-zero literal size + local snprintf_match = line:match('snprintf%s*%([^,]*,%s*([0-9]+)%s*,') + if snprintf_match and snprintf_match ~= '0' then + error( + filename, + linenum, + 'runtime/printf', + 3, + 'If you can, use sizeof(...) instead of ' .. snprintf_match .. ' as the 2nd arg to snprintf.' + ) + end + + -- Check for sprintf (use vim.regex for proper word boundaries) + local sprintf_regex = vim.regex([[\]]) + if sprintf_regex:match_str(line) then + error(filename, linenum, 'runtime/printf', 5, 'Use snprintf instead of sprintf.') + end + + -- Check for strncpy (use vim.regex for proper word boundaries) + local strncpy_regex = vim.regex([[\]]) + local strncpy_upper_regex = vim.regex([[\]]) + if strncpy_regex:match_str(line) then + error( + filename, + linenum, + 'runtime/printf', + 4, + 'Use xstrlcpy, xmemcpyz or snprintf instead of strncpy (unless this is from Vim)' + ) + elseif strncpy_upper_regex:match_str(line) then + error( + filename, + linenum, + 'runtime/printf', + 4, + 'Use xstrlcpy, xmemcpyz or snprintf instead of STRNCPY (unless this is from Vim)' + ) + end + + -- Check for strcpy (use vim.regex for proper word boundaries) + local strcpy_regex = vim.regex([[\]]) + if strcpy_regex:match_str(line) then + error( + filename, + linenum, + 'runtime/printf', + 4, + 'Use xstrlcpy, xmemcpyz or snprintf instead of strcpy' + ) + end + + -- Check for memset with wrong argument order: memset(buf, sizeof(buf), 0) + -- Pattern: memset(arg1, arg2, 0) where arg2 is NOT a valid fill value + local memset_start = line:find('memset%s*%([^)]*,%s*[^,]*,%s*0%s*%)') + if memset_start then + -- Extract the full memset call + local memset_part = line:sub(memset_start) + local first_comma = memset_part:find(',') + if first_comma then + local after_first = memset_part:sub(first_comma + 1) + local second_comma = after_first:find(',') + if second_comma then + local second_arg = vim.trim(after_first:sub(1, second_comma - 1)) + local first_arg = vim.trim(memset_part:match('memset%s*%(%s*([^,]*)%s*,')) + + -- Check if second_arg is NOT a simple literal value + if + second_arg ~= '' + and second_arg ~= "''" + and not second_arg:match('^%-?%d+$') + and not second_arg:match('^0x[0-9a-fA-F]+$') + then + error( + filename, + linenum, + 'runtime/memset', + 4, + 'Did you mean "memset(' .. first_arg .. ', 0, ' .. second_arg .. ')"?' + ) + end + end + end + end + + -- Detect variable-length arrays + -- Pattern: type varname[size]; where type is an identifier and varname starts with lowercase + local var_type = line:match('%s*(%w+)%s+') + if var_type and var_type ~= 'return' and var_type ~= 'delete' then + -- Look for array declaration pattern + local array_size = line:match('%w+%s+[a-z]%w*%s*%[([^%]]+)%]') + if array_size and not array_size:find('%]') then -- Ensure no nested brackets (multidimensional arrays) + -- Check if size is a compile-time constant + local is_const = true + + -- Split on common operators (space, +, -, *, /, <<, >>) + local tokens = vim.split(array_size, '[%s%+%-%*%/%>%<]+') + + for _, tok in ipairs(tokens) do + tok = vim.trim(tok) + if tok ~= '' then + -- Check for sizeof(...) and arraysize(...) or ARRAY_SIZE(...) patterns (before stripping parens) + local is_valid = tok:find('sizeof%(.+%)') + or tok:find('arraysize%(%w+%)') + or tok:find('ARRAY_SIZE%(.+%)') + + if not is_valid then + -- Strip leading and trailing parentheses for other checks + tok = tok:gsub('^%(*', ''):gsub('%)*$', '') + tok = vim.trim(tok) + + if tok ~= '' then + -- Allow: numeric literals, hex, k-prefixed constants, SCREAMING_CASE, sizeof, arraysize + is_valid = ( + tok:match('^%d+$') -- decimal number + or tok:match('^0x[0-9a-fA-F]+$') -- hex number + or tok:match('^k[A-Z0-9]') -- k-prefixed constant + or tok:match('^[A-Z][A-Z0-9_]*$') -- SCREAMING_CASE + or tok:match('^sizeof') -- sizeof(...) + or tok:match('^arraysize') + ) -- arraysize(...) + end + end + + if not is_valid then + is_const = false + break + end + end + end + + if not is_const then + error( + filename, + linenum, + 'runtime/arrays', + 1, + "Do not use variable-length arrays. Use an appropriately named ('k' followed by CamelCase) compile-time constant for the size." + ) + end + end + end + + -- Check for TRUE/FALSE (use vim.regex for proper word boundaries) + local true_regex = vim.regex([[\]]) + local false_regex = vim.regex([[\]]) + local maybe_regex = vim.regex([[\]]) + + if true_regex:match_str(line) then + error(filename, linenum, 'readability/bool', 4, 'Use true instead of TRUE.') + end + + if false_regex:match_str(line) then + error(filename, linenum, 'readability/bool', 4, 'Use false instead of FALSE.') + end + + -- Check for MAYBE + if maybe_regex:match_str(line) then + error(filename, linenum, 'readability/bool', 4, 'Use kNONE from TriState instead of MAYBE.') + end + + -- Detect preincrement/predecrement at start of line + if line:match('^%s*%+%+') or line:match('^%s*%-%-') then + error( + filename, + linenum, + 'readability/increment', + 5, + 'Do not use preincrement in statements, use postincrement instead' + ) + end + + -- Detect preincrement/predecrement in for(;; preincrement) + -- Look for pattern like "; ++var" or "; --var" + local last_semi_pos = 0 + for i = 1, #line do + if line:sub(i, i) == ';' then + last_semi_pos = i + end + end + + if last_semi_pos > 0 then + -- Check if there's a preincrement/predecrement after the last semicolon + local after_semi = line:sub(last_semi_pos + 1) + local op_pos = after_semi:find('%+%+') + if not op_pos then + op_pos = after_semi:find('%-%-') + end + if op_pos then + -- Found preincrement/predecrement after last semicolon + local expr_start = after_semi:sub(1, op_pos - 1):match('^%s*(.*)') + if not expr_start or expr_start == '' then + -- Nothing but whitespace before operator, check the expression + local expr_text = after_semi:sub(op_pos) + if not expr_text:find(';') and not expr_text:find(' = ') then + error( + filename, + linenum, + 'readability/increment', + 4, + 'Do not use preincrement in statements, including for(;; action)' + ) + end + end + end + end +end + +local function check_for_non_standard_constructs(filename, clean_lines, linenum, error) + local line = clean_lines.lines[linenum + 1] + + -- Check for printf format issues with %q and %N$ in quoted strings + -- Extract all quoted strings and check their format specifiers + for str in line:gmatch('"([^"]*)"') do + -- Check for %q format (deprecated) + if str:find('%%%-?%+?%s?%d*q') then + error( + filename, + linenum, + 'runtime/printf_format', + 3, + '"%q" in format strings is deprecated. Use "%" PRId64 instead.' + ) + end + + -- Check for %N$ format (unconventional positional specifier) + if str:find('%%%d+%$') then + error( + filename, + linenum, + 'runtime/printf_format', + 2, + '%N$ formats are unconventional. Try rewriting to avoid them.' + ) + end + end + + -- Check for storage class order (type before storage class modifier) + -- Match type keywords followed by storage class keywords + local type_keywords = { + 'const', + 'volatile', + 'void', + 'char', + 'short', + 'int', + 'long', + 'float', + 'double', + 'signed', + 'unsigned', + } + local storage_keywords = { 'register', 'static', 'extern', 'typedef' } + + for _, type_kw in ipairs(type_keywords) do + for _, storage_kw in ipairs(storage_keywords) do + local pattern = '\\<' .. type_kw .. '\\>\\s\\+\\<' .. storage_kw .. '\\>' + if vim.regex(pattern):match_str(line) then + error( + filename, + linenum, + 'build/storage_class', + 5, + 'Storage class (static, extern, typedef, etc) should be first.' + ) + return + end + end + end + + -- Check for endif comments + if line:match('^%s*#%s*endif%s*[^/\\s]+') then + error( + filename, + linenum, + 'build/endif_comment', + 5, + 'Uncommented text after #endif is non-standard. Use a comment.' + ) + end +end + +-- Nesting state classes +local BlockInfo = {} +BlockInfo.__index = BlockInfo + +function BlockInfo.new(seen_open_brace) + local self = setmetatable({}, BlockInfo) + self.seen_open_brace = seen_open_brace + self.open_parentheses = 0 + self.inline_asm = NO_ASM + return self +end + +local PreprocessorInfo = {} +PreprocessorInfo.__index = PreprocessorInfo + +function PreprocessorInfo.new(stack_before_if) + local self = setmetatable({}, PreprocessorInfo) + self.stack_before_if = stack_before_if + self.stack_before_else = {} + self.seen_else = false + return self +end + +local NestingState = {} +NestingState.__index = NestingState + +function NestingState.new() + local self = setmetatable({}, NestingState) + self.stack = {} + self.pp_stack = {} + return self +end + +function NestingState:seen_open_brace() + return #self.stack == 0 or self.stack[#self.stack].seen_open_brace +end + +function NestingState:update_preprocessor(line) + if line:match('^%s*#%s*(if|ifdef|ifndef)') then + table.insert(self.pp_stack, PreprocessorInfo.new(vim.deepcopy(self.stack))) + elseif line:match('^%s*#%s*(else|elif)') then + if #self.pp_stack > 0 then + if not self.pp_stack[#self.pp_stack].seen_else then + self.pp_stack[#self.pp_stack].seen_else = true + self.pp_stack[#self.pp_stack].stack_before_else = vim.deepcopy(self.stack) + end + self.stack = vim.deepcopy(self.pp_stack[#self.pp_stack].stack_before_if) + end + elseif line:match('^%s*#%s*endif') then + if #self.pp_stack > 0 then + if self.pp_stack[#self.pp_stack].seen_else then + self.stack = self.pp_stack[#self.pp_stack].stack_before_else + end + table.remove(self.pp_stack) + end + end +end + +function NestingState:update(clean_lines, linenum) + local line = clean_lines.elided[linenum + 1] + + self:update_preprocessor(line) + + if #self.stack > 0 then + local inner_block = self.stack[#self.stack] + local depth_change = select(2, line:gsub('%(', '')) - select(2, line:gsub('%)', '')) + inner_block.open_parentheses = inner_block.open_parentheses + depth_change + + if inner_block.inline_asm == NO_ASM or inner_block.inline_asm == END_ASM then + if depth_change ~= 0 and inner_block.open_parentheses == 1 and match_asm(line) then + inner_block.inline_asm = INSIDE_ASM + else + inner_block.inline_asm = NO_ASM + end + elseif inner_block.inline_asm == INSIDE_ASM and inner_block.open_parentheses == 0 then + inner_block.inline_asm = END_ASM + end + end + + while true do + local matched = line:match('^[^{;)}]*([{;)}])(.*)$') + if not matched then + break + end + + local token = matched:sub(1, 1) + if token == '{' then + if not self:seen_open_brace() then + self.stack[#self.stack].seen_open_brace = true + else + table.insert(self.stack, BlockInfo.new(true)) + if match_asm(line) then + self.stack[#self.stack].inline_asm = BLOCK_ASM + end + end + elseif token == ';' or token == ')' then + if not self:seen_open_brace() then + table.remove(self.stack) + end + else -- token == '}' + if #self.stack > 0 then + table.remove(self.stack) + end + end + line = matched:sub(2) + end +end + +-- Main processing functions +local function process_line( + filename, + clean_lines, + line, + nesting_state, + error, + extra_check_functions +) + local raw_lines = clean_lines.raw_lines + local init_lines = clean_lines.init_lines + + parse_nolint_suppressions(raw_lines[line + 1], line) + nesting_state:update(clean_lines, line) + + if + #nesting_state.stack > 0 and nesting_state.stack[#nesting_state.stack].inline_asm ~= NO_ASM + then + return + end + + check_for_multiline_comments_and_strings(filename, clean_lines, line, error) + check_for_old_style_comments(filename, init_lines[line + 1], line, error) + check_language(filename, clean_lines, line, error) + check_for_non_standard_constructs(filename, clean_lines, line, error) + check_posix_threading(filename, clean_lines, line, error) + check_memory_functions(filename, clean_lines, line, error) + check_os_functions(filename, clean_lines, line, error) + + for _, check_fn in ipairs(extra_check_functions or {}) do + check_fn(filename, clean_lines, line, error) + end +end + +local function process_file_data(filename, file_extension, lines, error, extra_check_functions) + -- Add marker lines + table.insert(lines, 1, '// marker so line numbers and indices both start at 1') + table.insert(lines, '// marker so line numbers end in a known way') + + local nesting_state = NestingState.new() + + reset_nolint_suppressions() + reset_known_error_suppressions() + + local init_lines = vim.deepcopy(lines) + + if cpplint_state.record_errors_file then + local function recorded_error(filename_, linenum, category, confidence, message) + if not is_error_suppressed_by_nolint(category, linenum) then + local key_start = math.max(1, linenum) + local key_end = math.min(#lines, linenum + 2) + local key_lines = {} + for i = key_start, key_end do + table.insert(key_lines, lines[i]) + end + local err = { filename_, key_lines, category } + cpplint_state.record_errors_file:write(vim.json.encode(err) .. '\n') + end + error(filename_, linenum, category, confidence, message) + end + error = recorded_error + end + + remove_multiline_comments(filename, lines, error) + local clean_lines = CleansedLines.new(lines, init_lines) + + for line = 0, clean_lines:num_lines() - 1 do + process_line(filename, clean_lines, line, nesting_state, error, extra_check_functions) + end + + if file_extension == 'h' then + check_for_header_guard(filename, lines, error) + check_includes(filename, lines, error) + if filename:match('/defs%.h$') or filename:match('_defs%.h$') then + check_non_symbols(filename, lines, error) + end + end + + check_for_bad_characters(filename, lines, error) +end + +local function process_file(filename, vlevel, extra_check_functions) + cpplint_state:set_verbose_level(vlevel) + + local lines + + if filename == '-' then + local stdin = io.read('*all') + lines = vim.split(stdin, '\n') + if cpplint_state.stdin_filename ~= '' then + filename = cpplint_state.stdin_filename + end + else + local ok, content = pcall(vim.fn.readfile, filename) + if not ok then + io.stderr:write("Skipping input '" .. filename .. "': Can't open for reading\n") + return + end + lines = content + end + + -- Remove trailing '\r' + for i, line in ipairs(lines) do + if line:sub(-1) == '\r' then + lines[i] = line:sub(1, -2) + end + end + + local file_extension = filename:match('^.+%.(.+)$') or '' + + if filename ~= '-' and not valid_extensions[file_extension] then + local ext_list = {} + for ext, _ in pairs(valid_extensions) do + table.insert(ext_list, '.' .. ext) + end + io.stderr:write( + 'Ignoring ' .. filename .. '; only linting ' .. table.concat(ext_list, ', ') .. ' files\n' + ) + else + process_file_data(filename, file_extension, lines, error_func, extra_check_functions) + end +end + +-- Main function +local function main(args) + local filenames, opts = parse_arguments(args) + + cpplint_state:set_output_format(opts.output_format) + cpplint_state:set_verbose_level(opts.verbose_level) + cpplint_state:set_filters(opts.filters) + cpplint_state:set_counting_style(opts.counting_style) + valid_extensions = {} + for _, ext in ipairs(opts.extensions) do + valid_extensions[ext] = true + end + + cpplint_state:suppress_errors_from(opts.suppress_errors_file) + cpplint_state:record_errors_to(opts.record_errors_file) + cpplint_state.stdin_filename = opts.stdin_filename + + cpplint_state:reset_error_counts() + + for _, filename in ipairs(filenames) do + process_file(filename, cpplint_state.verbose_level) + end + + cpplint_state:print_error_counts() + + if cpplint_state.record_errors_file then + cpplint_state.record_errors_file:close() + end + + vim.cmd.cquit(cpplint_state.error_count > 0 and 1 or 0) +end + +-- Export main function +main(_G.arg) diff --git a/src/clint.py b/src/clint.py deleted file mode 100755 index 20fc85a541..0000000000 --- a/src/clint.py +++ /dev/null @@ -1,2414 +0,0 @@ -#!/usr/bin/env python3 -# -# https://github.com/cpplint/cpplint -# -# Copyright (c) 2009 Google Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of Google Inc. nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Lints C files in the Neovim source tree. - -This can get very confused by /* and // inside strings! We do a small hack, -which is to ignore //'s with "'s after them on the same line, but it is far -from perfect (in either direction). -""" - - -import copy -import getopt -import os -import re -import string -import sys -import json -import collections - - -_USAGE = """ -Syntax: clint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] - [--counting=total|toplevel|detailed] [--root=subdir] - [--linelength=digits] [--record-errors=file] - [--suppress-errors=file] [--stdin-filename=filename] - [file] ... - - The style guidelines this tries to follow are those in - https://neovim.io/doc/user/dev_style.html#dev-style - - Note: This is Google's https://github.com/cpplint/cpplint modified for use - with the Neovim project. - - Every problem is given a confidence score from 1-5, with 5 meaning we are - certain of the problem, and 1 meaning it could be a legitimate construct. - This will miss some errors, and is not a substitute for a code review. - - To suppress false-positive errors of a certain category, add a - 'NOLINT(category)' comment to the line. NOLINT or NOLINT(*) - suppresses errors of all categories on that line. - - The files passed in will be linted; at least one file must be provided. - Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the - extensions with the --extensions flag. - - Flags: - - output=vs7 - By default, the output is formatted to ease emacs parsing. Visual Studio - compatible output (vs7) may also be used. Other formats are unsupported. - - verbose=# - Specify a number 0-5 to restrict errors to certain verbosity levels. - - filter=-x,+y,... - Specify a comma-separated list of category-filters to apply: only - error messages whose category names pass the filters will be printed. - (Category names are printed with the message and look like - "[whitespace/indent]".) Filters are evaluated left to right. - "-FOO" and "FOO" means "do not print categories that start with FOO". - "+FOO" means "do print categories that start with FOO". - - Examples: --filter=-whitespace,+whitespace/braces - --filter=whitespace,runtime/printf,+runtime/printf_format - --filter=-,+build/include_what_you_use - - To see a list of all the categories used in cpplint, pass no arg: - --filter= - - counting=total|toplevel|detailed - The total number of errors found is always printed. If - 'toplevel' is provided, then the count of errors in each of - the top-level categories like 'build' and 'whitespace' will - also be printed. If 'detailed' is provided, then a count - is provided for each category. - - root=subdir - The root directory used for deriving header guard CPP variable. - By default, the header guard CPP variable is calculated as the relative - path to the directory that contains .git, .hg, or .svn. When this flag - is specified, the relative path is calculated from the specified - directory. If the specified directory does not exist, this flag is - ignored. - - Examples: - Assuing that src/.git exists, the header guard CPP variables for - src/chrome/browser/ui/browser.h are: - - No flag => CHROME_BROWSER_UI_BROWSER_H_ - --root=chrome => BROWSER_UI_BROWSER_H_ - --root=chrome/browser => UI_BROWSER_H_ - - linelength=digits - This is the allowed line length for the project. The default value is - 80 characters. - - Examples: - --linelength=120 - - extensions=extension,extension,... - The allowed file extensions that cpplint will check - - Examples: - --extensions=hpp,cpp - - record-errors=file - Record errors to the given location. This file may later be used for error - suppression using suppress-errors flag. - - suppress-errors=file - Errors listed in the given file will not be reported. - - stdin-filename=filename - Use specified filename when reading from stdin (file "-"). -""" - -# We categorize each error message we print. Here are the categories. -# We want an explicit list so we can list them all in cpplint --filter=. -# If you add a new error message with a new category, add it to the list -# here! cpplint_unittest.py should tell you if you forget to do this. -_ERROR_CATEGORIES = [ - 'build/endif_comment', - 'build/header_guard', - 'build/include_defs', - 'build/defs_header', - 'build/printf_format', - 'build/storage_class', - 'build/init_macro', - 'readability/bool', - 'readability/multiline_comment', - 'readability/multiline_string', - 'readability/nul', - 'readability/todo', - 'readability/utf8', - 'readability/increment', - 'runtime/arrays', - 'runtime/int', - 'runtime/memset', - 'runtime/printf', - 'runtime/printf_format', - 'runtime/threadsafe_fn', - 'runtime/deprecated', - 'whitespace/comments', - 'whitespace/indent', - 'whitespace/operators', - 'whitespace/todo', - 'whitespace/cast', -] - -# The default state of the category filter. This is overridden by the --filter= -# flag. By default all errors are on, so only add here categories that should be -# off by default (i.e., categories that must be enabled by the --filter= flags). -# All entries here should start with a '-' or '+', as in the --filter= flag. -_DEFAULT_FILTERS = [] - -# These constants define the current inline assembly state -_NO_ASM = 0 # Outside of inline assembly block -_INSIDE_ASM = 1 # Inside inline assembly block -_END_ASM = 2 # Last line of inline assembly block -_BLOCK_ASM = 3 # The whole block is an inline assembly block - -# Match start of assembly blocks -_MATCH_ASM = re.compile(r'^\s*(?:asm|_asm|__asm|__asm__)' - r'(?:\s+(volatile|__volatile__))?' - r'\s*[{(]') - - -_regexp_compile_cache = {} - -# Finds occurrences of NOLINT or NOLINT(...). -_RE_SUPPRESSION = re.compile(r'\bNOLINT\b(\([^)]*\))?') - -# {str, set(int)}: a map from error categories to sets of linenumbers -# on which those errors are expected and should be suppressed. -_error_suppressions = {} - -# {(str, int)}: a set of error categories and line numbers which are expected to -# be suppressed -_error_suppressions_2 = set() - -# The allowed line length of files. -# This is set by --linelength flag. -_line_length = 100 - -# The allowed extensions for file names -# This is set by --extensions flag. -_valid_extensions = {'c', 'h'} - -_RE_COMMENTLINE = re.compile(r'^\s*//') - - -def ParseNolintSuppressions(raw_line, linenum): - """Updates the global list of error-suppressions. - - Parses any NOLINT comments on the current line, updating the global - error_suppressions store. Reports an error if the NOLINT comment - was malformed. - - Args: - raw_line: str, the line of input text, with comments. - linenum: int, the number of the current line. - """ - # FIXME(adonovan): "NOLINT(" is misparsed as NOLINT(*). - matched = _RE_SUPPRESSION.search(raw_line) - if matched: - category = matched.group(1) - if category in (None, '(*)'): # => "suppress all" - _error_suppressions.setdefault(None, set()).add(linenum) - else: - if category.startswith('(') and category.endswith(')'): - category = category[1:-1] - if category in _ERROR_CATEGORIES: - _error_suppressions.setdefault( - category, set()).add(linenum) - - -def ParseKnownErrorSuppressions(filename, raw_lines, linenum): - """Updates the global list of error-suppressions from suppress-file. - - Args: - filename: str, the name of the input file. - raw_lines: list, all file lines - linenum: int, the number of the current line. - """ - key = tuple(raw_lines[linenum - 1 if linenum else 0:linenum + 2]) - if key in _cpplint_state.suppressed_errors[filename]: - for category in _cpplint_state.suppressed_errors[filename][key]: - _error_suppressions_2.add((category, linenum)) - - -def ResetNolintSuppressions(): - "Resets the set of NOLINT suppressions to empty." - _error_suppressions.clear() - - -def ResetKnownErrorSuppressions(): - "Resets the set of suppress-errors=file suppressions to empty." - _error_suppressions_2.clear() - - -def IsErrorSuppressedByNolint(category, linenum): - """Returns true if the specified error category is suppressed on this line. - - Consults the global error_suppressions map populated by - ParseNolintSuppressions/ResetNolintSuppressions. - - Args: - category: str, the category of the error. - linenum: int, the current line number. - Returns: - bool, True iff the error should be suppressed due to a NOLINT comment. - """ - return (linenum in _error_suppressions.get(category, set()) or - linenum in _error_suppressions.get(None, set())) - - -def IsErrorInSuppressedErrorsList(category, linenum): - """Returns true if the specified error is suppressed by suppress-errors=file - - Args: - category: str, the category of the error. - linenum: int, the current line number. - Returns: - bool, True iff the error should be suppressed due to presence in - suppressions file. - """ - return (category, linenum) in _error_suppressions_2 - - -def Match(pattern, s): - """Matches the string with the pattern, caching the compiled regexp.""" - # The regexp compilation caching is inlined in both Match and Search for - # performance reasons; factoring it out into a separate function turns out - # to be noticeably expensive. - if pattern not in _regexp_compile_cache: - _regexp_compile_cache[pattern] = re.compile(pattern) - return _regexp_compile_cache[pattern].match(s) - - -def Search(pattern, s): - """Searches the string for the pattern, caching the compiled regexp.""" - if pattern not in _regexp_compile_cache: - _regexp_compile_cache[pattern] = re.compile(pattern) - return _regexp_compile_cache[pattern].search(s) - - -class _CppLintState: - - """Maintains module-wide state..""" - - def __init__(self): - self.verbose_level = 1 # global setting. - self.error_count = 0 # global count of reported errors - # filters to apply when emitting error messages - self.filters = _DEFAULT_FILTERS[:] - self.counting = 'total' # In what way are we counting errors? - self.errors_by_category = {} # string to int dict storing error counts - self.stdin_filename = '' - - # output format: - # "emacs" - format that emacs can parse (default) - # "vs7" - format that Microsoft Visual Studio 7 can parse - self.output_format = 'emacs' - - self.record_errors_file = None - self.suppressed_errors = collections.defaultdict( - lambda: collections.defaultdict(set)) - - def SetOutputFormat(self, output_format): - """Sets the output format for errors.""" - self.output_format = output_format - - def SetVerboseLevel(self, level): - """Sets the module's verbosity, and returns the previous setting.""" - last_verbose_level = self.verbose_level - self.verbose_level = level - return last_verbose_level - - def SetCountingStyle(self, counting_style): - """Sets the module's counting options.""" - self.counting = counting_style - - def SetFilters(self, filters): - """Sets the error-message filters. - - These filters are applied when deciding whether to emit a given - error message. - - Args: - filters: A string of comma-separated filters. - E.g. "+whitespace/indent". - Each filter should start with + or -; else we die. - - Raises: - ValueError: The comma-separated filters did not all start with - '+' or '-'. - E.g. "-,+whitespace,-whitespace/indent,whitespace/bad" - """ - # Default filters always have less priority than the flag ones. - self.filters = _DEFAULT_FILTERS[:] - for filt in filters.split(','): - clean_filt = filt.strip() - if clean_filt: - self.filters.append(clean_filt) - for filt in self.filters: - if not (filt.startswith('+') or filt.startswith('-')): - raise ValueError('Every filter in --filters must start with ' - '+ or - (%s does not)' % filt) - - def ResetErrorCounts(self): - """Sets the module's error statistic back to zero.""" - self.error_count = 0 - self.errors_by_category = {} - - def IncrementErrorCount(self, category): - """Bumps the module's error statistic.""" - self.error_count += 1 - if self.counting in ('toplevel', 'detailed'): - if self.counting != 'detailed': - category = category.split('/')[0] - if category not in self.errors_by_category: - self.errors_by_category[category] = 0 - self.errors_by_category[category] += 1 - - def PrintErrorCounts(self): - """Print a summary of errors by category, and the total.""" - for category, count in self.errors_by_category.items(): - sys.stdout.write('Category \'%s\' errors found: %d\n' % - (category, count)) - if self.error_count: - sys.stdout.write('Total errors found: %d\n' % self.error_count) - - def SuppressErrorsFrom(self, fname): - """Open file and read a list of suppressed errors from it""" - if fname is None: - return - try: - with open(fname) as fp: - for line in fp: - fname, lines, category = json.loads(line) - lines = tuple(lines) - self.suppressed_errors[fname][lines].add(category) - except OSError: - pass - - def RecordErrorsTo(self, fname): - """Open file with suppressed errors for writing""" - if fname is None: - return - self.record_errors_file = open(fname, 'w') - - -_cpplint_state = _CppLintState() - - -def _OutputFormat(): - """Gets the module's output format.""" - return _cpplint_state.output_format - - -def _SetOutputFormat(output_format): - """Sets the module's output format.""" - _cpplint_state.SetOutputFormat(output_format) - - -def _VerboseLevel(): - """Returns the module's verbosity setting.""" - return _cpplint_state.verbose_level - - -def _SetVerboseLevel(level): - """Sets the module's verbosity, and returns the previous setting.""" - return _cpplint_state.SetVerboseLevel(level) - - -def _SetCountingStyle(level): - """Sets the module's counting options.""" - _cpplint_state.SetCountingStyle(level) - - -def _SuppressErrorsFrom(fname): - """Sets the file containing suppressed errors.""" - _cpplint_state.SuppressErrorsFrom(fname) - - -def _RecordErrorsTo(fname): - """Sets the file containing suppressed errors to write to.""" - _cpplint_state.RecordErrorsTo(fname) - - -def _Filters(): - """Returns the module's list of output filters, as a list.""" - return _cpplint_state.filters - - -def _SetFilters(filters): - """Sets the module's error-message filters. - - These filters are applied when deciding whether to emit a given - error message. - - Args: - filters: A string of comma-separated filters (eg "whitespace/indent"). - Each filter should start with + or -; else we die. - """ - _cpplint_state.SetFilters(filters) - - -class FileInfo: - - """Provides utility functions for filenames. - - FileInfo provides easy access to the components of a file's path - relative to the project root. - """ - - def __init__(self, filename): - self._filename = filename - - def FullName(self): - """Make Windows paths like Unix.""" - abspath = str(os.path.abspath(self._filename)) - return abspath.replace('\\', '/') - - def RelativePath(self): - """FullName with /src/nvim/ chopped off.""" - fullname = self.FullName() - - if os.path.exists(fullname): - project_dir = os.path.dirname(fullname) - - root_dir = os.path.dirname(fullname) - while (root_dir != os.path.dirname(root_dir) and - not os.path.exists(os.path.join(root_dir, ".git"))): - root_dir = os.path.dirname(root_dir) - - if os.path.exists(os.path.join(root_dir, ".git")): - root_dir = os.path.join(root_dir, "src", "nvim") - prefix = os.path.commonprefix([root_dir, project_dir]) - return fullname[len(prefix) + 1:] - - # Don't know what to do; header guard warnings may be wrong... - return fullname - -def _ShouldPrintError(category, confidence, linenum): - """If confidence >= verbose, category passes filter and isn't suppressed.""" - - # There are three ways we might decide not to print an error message: - # a "NOLINT(category)" comment appears in the source, - # the verbosity level isn't high enough, or the filters filter it out. - if IsErrorSuppressedByNolint(category, linenum): - return False - if IsErrorInSuppressedErrorsList(category, linenum): - return False - if confidence < _cpplint_state.verbose_level: - return False - - is_filtered = False - for one_filter in _Filters(): - if one_filter.startswith('-'): - if category.startswith(one_filter[1:]): - is_filtered = True - elif one_filter.startswith('+'): - if category.startswith(one_filter[1:]): - is_filtered = False - else: - assert False # should have been checked for in SetFilter. - if is_filtered: - return False - - return True - - -def Error(filename, linenum, category, confidence, message): - """Logs the fact we've found a lint error. - - We log where the error was found, and also our confidence in the error, - that is, how certain we are this is a legitimate style regression, and - not a misidentification or a use that's sometimes justified. - - False positives can be suppressed by the use of - "cpplint(category)" comments on the offending line. These are - parsed into _error_suppressions. - - Args: - filename: The name of the file containing the error. - linenum: The number of the line containing the error. - category: A string used to describe the "category" this bug - falls under: "whitespace", say, or "runtime". Categories - may have a hierarchy separated by slashes: "whitespace/indent". - confidence: A number from 1-5 representing a confidence score for - the error, with 5 meaning that we are certain of the problem, - and 1 meaning that it could be a legitimate construct. - message: The error message. - """ - if _ShouldPrintError(category, confidence, linenum): - _cpplint_state.IncrementErrorCount(category) - if _cpplint_state.output_format == 'vs7': - sys.stdout.write('%s(%s): %s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - elif _cpplint_state.output_format == 'eclipse': - sys.stdout.write('%s:%s: warning: %s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - elif _cpplint_state.output_format == 'gh_action': - sys.stdout.write('::error file=%s,line=%s::%s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - else: - sys.stdout.write('%s:%s: %s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - - -# Matches standard C++ escape sequences per 2.13.2.3 of the C++ standard. -_RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile( - r'\\([abfnrtv?"\\\']|\d+|x[0-9a-fA-F]+)') -# Matches strings. Escape codes should already be removed by ESCAPES. -_RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES = re.compile(r'"([^"]*)"') -# Matches characters. Escape codes should already be removed by ESCAPES. -_RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES = re.compile(r"'(.)'") -# Matches multi-line C++ comments. -# This RE is a little bit more complicated than one might expect, because we -# have to take care of space removals tools so we can handle comments inside -# statements better. -# The current rule is: We only clear spaces from both sides when we're at the -# end of the line. Otherwise, we try to remove spaces from the right side, -# if this doesn't work we try on left side but only if there's a non-character -# on the right. -_RE_PATTERN_CLEANSE_LINE_C_COMMENTS = re.compile( - r"""(\s*/\*.*\*/\s*$| - /\*.*\*/\s+| - \s+/\*.*\*/(?=\W)| - /\*.*\*/)""", re.VERBOSE) - - -def IsCppString(line): - """Does line terminate so, that the next symbol is in string constant. - - This function does not consider single-line nor multi-line comments. - - Args: - line: is a partial line of code starting from the 0..n. - - Returns: - True, if next character appended to 'line' is inside a - string constant. - """ - - line = line.replace(r'\\', 'XX') # after this, \\" does not match to \" - return ((line.count('"') - line.count(r'\"') - line.count("'\"'")) & 1) == 1 - - -def FindNextMultiLineCommentStart(lines, lineix): - """Find the beginning marker for a multiline comment.""" - while lineix < len(lines): - if lines[lineix].strip().startswith('/*'): - # Only return this marker if the comment goes beyond this line - if lines[lineix].strip().find('*/', 2) < 0: - return lineix - lineix += 1 - return len(lines) - - -def FindNextMultiLineCommentEnd(lines, lineix): - """We are inside a comment, find the end marker.""" - while lineix < len(lines): - if lines[lineix].strip().endswith('*/'): - return lineix - lineix += 1 - return len(lines) - - -def RemoveMultiLineCommentsFromRange(lines, begin, end): - """Clears a range of lines for multi-line comments.""" - # Having // dummy comments makes the lines non-empty, so we will not get - # unnecessary blank line warnings later in the code. - for i in range(begin, end): - lines[i] = '// dummy' - - -def RemoveMultiLineComments(filename, lines, error): - """Removes multiline (c-style) comments from lines.""" - lineix = 0 - while lineix < len(lines): - lineix_begin = FindNextMultiLineCommentStart(lines, lineix) - if lineix_begin >= len(lines): - return - lineix_end = FindNextMultiLineCommentEnd(lines, lineix_begin) - if lineix_end >= len(lines): - error(filename, lineix_begin + 1, 'readability/multiline_comment', - 5, 'Could not find end of multi-line comment') - return - RemoveMultiLineCommentsFromRange(lines, lineix_begin, lineix_end + 1) - lineix = lineix_end + 1 - - -def CleanseComments(line): - """Removes //-comments and single-line C-style /* */ comments. - - Args: - line: A line of C++ source. - - Returns: - The line with single-line comments removed. - """ - commentpos = line.find('//') - if commentpos != -1 and not IsCppString(line[:commentpos]): - line = line[:commentpos].rstrip() - # get rid of /* ... */ - return _RE_PATTERN_CLEANSE_LINE_C_COMMENTS.sub('', line) - - -class CleansedLines: - - """Holds 5 copies of all lines with different preprocessing applied to them. - - 1) elided member contains lines without strings and comments, - 2) lines member contains lines without comments, and - 3) raw_lines member contains all the lines with multiline comments replaced. - 4) init_lines member contains all the lines without processing. - 5) elided_with_space_strings is like elided, but with string literals - looking like `" "`. - All these three members are of , and of the same length. - """ - - def __init__(self, lines, init_lines): - self.elided = [] - self.lines = [] - self.raw_lines = lines - self.num_lines = len(lines) - self.init_lines = init_lines - self.lines_without_raw_strings = lines - self.elided_with_space_strings = [] - for linenum in range(len(self.lines_without_raw_strings)): - self.lines.append(CleanseComments( - self.lines_without_raw_strings[linenum])) - elided = self._CollapseStrings( - self.lines_without_raw_strings[linenum]) - self.elided.append(CleanseComments(elided)) - elided = CleanseComments(self._CollapseStrings( - self.lines_without_raw_strings[linenum], True)) - self.elided_with_space_strings.append(elided) - - def NumLines(self): - """Returns the number of lines represented.""" - return self.num_lines - - @staticmethod - def _CollapseStrings(elided, keep_spaces=False): - """Collapses strings and chars on a line to simple "" or '' blocks. - - We nix strings first so we're not fooled by text like '"http://"' - - Args: - elided: The line being processed. - keep_spaces: If true, collapse to - - Returns: - The line with collapsed strings. - """ - if not _RE_PATTERN_INCLUDE.match(elided): - # Remove escaped characters first to make quote/single quote - # collapsing basic. Things that look like escaped characters - # shouldn't occur outside of strings and chars. - elided = _RE_PATTERN_CLEANSE_LINE_ESCAPES.sub( - '' if not keep_spaces else lambda m: ' ' * len(m.group(0)), - elided) - elided = _RE_PATTERN_CLEANSE_LINE_SINGLE_QUOTES.sub( - "''" if not keep_spaces - else lambda m: "'" + (' ' * len(m.group(1))) + "'", - elided) - elided = _RE_PATTERN_CLEANSE_LINE_DOUBLE_QUOTES.sub( - '""' if not keep_spaces - else lambda m: '"' + (' ' * len(m.group(1))) + '"', - elided) - return elided - - -BRACES = { - '(': ')', - '{': '}', - '[': ']', -} - - -def FindEndOfExpressionInLine(line, startpos, depth, startchar, endchar): - """Find the position just after the matching endchar. - - Args: - line: a CleansedLines line. - startpos: start searching at this position. - depth: nesting level at startpos. - startchar: expression opening character. - endchar: expression closing character. - - Returns: - On finding matching endchar: (index just after matching endchar, 0) - Otherwise: (-1, new depth at end of this line) - """ - for i in range(startpos, len(line)): - if line[i] == startchar: - depth += 1 - elif line[i] == endchar: - depth -= 1 - if depth == 0: - return (i + 1, 0) - return (-1, depth) - - -def CloseExpression(clean_lines, linenum, pos): - """If input points to ( or { or [, finds the position that closes it. - - If lines[linenum][pos] points to a '(' or '{' or '[', finds the - linenum/pos that correspond to the closing of the expression. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - pos: A position on the line. - - Returns: - A tuple (line, linenum, pos) pointer *past* the closing brace, or - (line, len(lines), -1) if we never find a close. Note we ignore - strings and comments when matching; and the line we return is the - 'cleansed' line at linenum. - """ - - line = clean_lines.elided[linenum] - startchar = line[pos] - if startchar not in BRACES: - return (line, clean_lines.NumLines(), -1) - endchar = BRACES[startchar] - - # Check first line - (end_pos, num_open) = FindEndOfExpressionInLine( - line, pos, 0, startchar, endchar) - if end_pos > -1: - return (line, linenum, end_pos) - - # Continue scanning forward - while linenum < clean_lines.NumLines() - 1: - linenum += 1 - line = clean_lines.elided[linenum] - (end_pos, num_open) = FindEndOfExpressionInLine( - line, 0, num_open, startchar, endchar) - if end_pos > -1: - return (line, linenum, end_pos) - - # Did not find endchar before end of file, give up - return (line, clean_lines.NumLines(), -1) - - -def CheckForHeaderGuard(filename, lines, error): - """Checks that the file contains "#pragma once". - - Args: - filename: The name of the C++ header file. - lines: An array of strings, each representing a line of the file. - error: The function to call with any errors found. - """ - if filename.endswith('.c.h') or FileInfo(filename).RelativePath() in { - 'func_attr.h', - }: - return - - if "#pragma once" not in lines: - error(filename, 0, 'build/header_guard', 5, - 'No "#pragma once" found in header') - - -def CheckIncludes(filename, lines, error): - """Checks that headers only include _defs headers. - - Args: - filename: The name of the C++ header file. - lines: An array of strings, each representing a line of the file. - error: The function to call with any errors found. - """ - if (filename.endswith('.c.h') - or filename.endswith('.in.h') - or FileInfo(filename).RelativePath() in { - 'func_attr.h', - 'os/pty_proc.h', - }): - return - - check_includes_ignore = [ - "src/nvim/api/private/validate.h", - "src/nvim/assert_defs.h", - "src/nvim/channel.h", - "src/nvim/charset.h", - "src/nvim/eval/typval.h", - "src/nvim/event/multiqueue.h", - "src/nvim/garray.h", - "src/nvim/globals.h", - "src/nvim/highlight.h", - "src/nvim/lua/executor.h", - "src/nvim/main.h", - "src/nvim/mark.h", - "src/nvim/msgpack_rpc/channel_defs.h", - "src/nvim/msgpack_rpc/unpacker.h", - "src/nvim/option.h", - "src/nvim/os/pty_conpty_win.h", - "src/nvim/os/pty_proc_win.h", - ] - - skip_headers = [ - "auto/config.h", - "klib/klist.h", - "klib/kvec.h", - "mpack/mpack_core.h", - "mpack/object.h", - "nvim/func_attr.h", - "termkey/termkey.h", - "vterm/vterm.h", - "xdiff/xdiff.h", - ] - - for i in check_includes_ignore: - if filename.endswith(i): - return - - for i, line in enumerate(lines): - matched = Match(r'#\s*include\s*"([^"]*)"', line) - if matched: - name = matched.group(1) - if name in skip_headers: - continue - if (not name.endswith('.h.generated.h') and - not name.endswith('/defs.h') and - not name.endswith('_defs.h') and - not name.endswith('.h.inline.generated.h') and - not name.endswith('_defs.generated.h') and - not name.endswith('_enum.generated.h')): - error(filename, i, 'build/include_defs', 5, - 'Headers should not include non-"_defs" headers') - - -def CheckNonSymbols(filename, lines, error): - """Checks that a _defs.h header only contains non-symbols. - - Args: - filename: The name of the C++ header file. - lines: An array of strings, each representing a line of the file. - error: The function to call with any errors found. - """ - for i, line in enumerate(lines): - # Only a check against extern variables for now. - if line.startswith('EXTERN ') or line.startswith('extern '): - error(filename, i, 'build/defs_header', 5, - '"_defs" headers should not contain extern variables') - - -def CheckForBadCharacters(filename, lines, error): - """Logs an error for each line containing bad characters. - - Two kinds of bad characters: - - 1. Unicode replacement characters: These indicate that either the file - contained invalid UTF-8 (likely) or Unicode replacement characters (which - it shouldn't). Note that it's possible for this to throw off line - numbering if the invalid UTF-8 occurred adjacent to a newline. - - 2. NUL bytes. These are problematic for some tools. - - Args: - filename: The name of the current file. - lines: An array of strings, each representing a line of the file. - error: The function to call with any errors found. - """ - for linenum, line in enumerate(lines): - if '\ufffd' in line: - error(filename, linenum, 'readability/utf8', 5, - 'Line contains invalid UTF-8' - ' (or Unicode replacement character).') - if '\0' in line: - error(filename, linenum, 'readability/nul', - 5, 'Line contains NUL byte.') - - -def CheckForMultilineCommentsAndStrings(filename, clean_lines, linenum, error): - """Logs an error if we see /* ... */ or "..." that extend past one line. - - /* ... */ comments are legit inside macros, for one line. - Otherwise, we prefer // comments, so it's ok to warn about the - other. Likewise, it's ok for strings to extend across multiple - lines, as long as a line continuation character (backslash) - terminates each line. Although not currently prohibited by the C++ - style guide, it's ugly and unnecessary. We don't do well with either - in this lint program, so we warn about both. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - line = clean_lines.elided[linenum] - - # Remove all \\ (escaped backslashes) from the line. They are OK, and the - # second (escaped) slash may trigger later \" detection erroneously. - line = line.replace('\\\\', '') - - if line.count('/*') > line.count('*/'): - error(filename, linenum, 'readability/multiline_comment', 5, - 'Complex multi-line /*...*/-style comment found. ' - 'Lint may give bogus warnings. ' - 'Consider replacing these with //-style comments, ' - 'with #if 0...#endif, ' - 'or with more clearly structured multi-line comments.') - - if (line.count('"') - line.count('\\"')) % 2: - error(filename, linenum, 'readability/multiline_string', 5, - 'Multi-line string ("...") found. This lint script doesn\'t ' - 'do well with such strings, and may give bogus warnings. ' - 'Use C++11 raw strings or concatenation instead.') - - -def CheckForOldStyleComments(filename, line, linenum, error): - """Logs an error if we see /*-style comment - - Args: - filename: The name of the current file. - line: The text of the line to check. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - # hack: allow /* inside comment line. Could be extended to allow them inside - # any // comment. - if line.find('/*') >= 0 and line[-1] != '\\' and not _RE_COMMENTLINE.match(line): - error(filename, linenum, 'readability/old_style_comment', 5, - '/*-style comment found, it should be replaced with //-style. ' - '/*-style comments are only allowed inside macros. ' - 'Note that you should not use /*-style comments to document ' - 'macros itself, use doxygen-style comments for this.') - - -threading_list = ( - ('asctime(', 'os_asctime_r('), - ('ctime(', 'os_ctime_r('), - ('getgrgid(', 'os_getgrgid_r('), - ('getgrnam(', 'os_getgrnam_r('), - ('getlogin(', 'os_getlogin_r('), - ('getpwnam(', 'os_getpwnam_r('), - ('getpwuid(', 'os_getpwuid_r('), - ('gmtime(', 'os_gmtime_r('), - ('localtime(', 'os_localtime_r('), - ('strtok(', 'os_strtok_r('), - ('ttyname(', 'os_ttyname_r('), - ('asctime_r(', 'os_asctime_r('), - ('ctime_r(', 'os_ctime_r('), - ('getgrgid_r(', 'os_getgrgid_r('), - ('getgrnam_r(', 'os_getgrnam_r('), - ('getlogin_r(', 'os_getlogin_r('), - ('getpwnam_r(', 'os_getpwnam_r('), - ('getpwuid_r(', 'os_getpwuid_r('), - ('gmtime_r(', 'os_gmtime_r('), - ('localtime_r(', 'os_localtime_r('), - ('strtok_r(', 'os_strtok_r('), - ('ttyname_r(', 'os_ttyname_r('), -) - - -def CheckPosixThreading(filename, clean_lines, linenum, error): - """Checks for calls to thread-unsafe functions. - - Much code has been originally written without consideration of - multi-threading. Also, engineers are relying on their old experience; - they have learned posix before threading extensions were added. These - tests guide the engineers to use thread-safe functions (when using - posix directly). - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - line = clean_lines.elided[linenum] - for single_thread_function, multithread_safe_function in threading_list: - ix = line.find(single_thread_function) - # Comparisons made explicit for clarity -- pylint: - # disable=g-explicit-bool-comparison - if ix >= 0 and (ix == 0 or (not line[ix - 1].isalnum() and - line[ix - 1] not in ('_', '.', '>'))): - error(filename, linenum, 'runtime/threadsafe_fn', 2, - 'Use ' + multithread_safe_function + - '...) instead of ' + single_thread_function + - '...). If it is missing, consider implementing it;' + - ' see os_localtime_r for an example.') - - -memory_functions = ( - ('malloc(', 'xmalloc('), - ('calloc(', 'xcalloc('), - ('realloc(', 'xrealloc('), - ('strdup(', 'xstrdup('), - ('free(', 'xfree('), -) -memory_ignore_pattern = re.compile(r'src/nvim/memory.c$') - - -def CheckMemoryFunctions(filename, clean_lines, linenum, error): - """Checks for calls to invalid functions. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - if memory_ignore_pattern.search(filename): - return - line = clean_lines.elided[linenum] - for function, suggested_function in memory_functions: - ix = line.find(function) - # Comparisons made explicit for clarity -- pylint: - # disable=g-explicit-bool-comparison - if ix >= 0 and (ix == 0 or (not line[ix - 1].isalnum() and - line[ix - 1] not in ('_', '.', '>'))): - error(filename, linenum, 'runtime/memory_fn', 2, - 'Use ' + suggested_function + - '...) instead of ' + function + '...).') - - -os_functions = ( - ('setenv(', 'os_setenv('), - ('getenv(', 'os_getenv('), - ('_wputenv(', 'os_setenv('), - ('_putenv_s(', 'os_setenv('), - ('putenv(', 'os_setenv('), - ('unsetenv(', 'os_unsetenv('), -) - - -def CheckOSFunctions(filename, clean_lines, linenum, error): - """Checks for calls to invalid functions. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - line = clean_lines.elided[linenum] - for function, suggested_function in os_functions: - ix = line.find(function) - # Comparisons made explicit for clarity -- pylint: - # disable=g-explicit-bool-comparison - if ix >= 0 and (ix == 0 or (not line[ix - 1].isalnum() and - line[ix - 1] not in ('_', '.', '>'))): - error(filename, linenum, 'runtime/os_fn', 2, - 'Use ' + suggested_function + - '...) instead of ' + function + '...).') - - -class _BlockInfo: - - """Stores information about a generic block of code.""" - - def __init__(self, seen_open_brace): - self.seen_open_brace = seen_open_brace - self.open_parentheses = 0 - self.inline_asm = _NO_ASM - - -class _PreprocessorInfo: - - """Stores checkpoints of nesting stacks when #if/#else is seen.""" - - def __init__(self, stack_before_if): - # The entire nesting stack before #if - self.stack_before_if = stack_before_if - - # The entire nesting stack up to #else - self.stack_before_else = [] - - # Whether we have already seen #else or #elif - self.seen_else = False - - -class _NestingState: - - """Holds states related to parsing braces.""" - - def __init__(self): - # Stack for tracking all braces. An object is pushed whenever we - # see a "{", and popped when we see a "}". Only 1 type of - # object is possible: - # - _BlockInfo: some type of block. - self.stack = [] - - # Stack of _PreprocessorInfo objects. - self.pp_stack = [] - - def SeenOpenBrace(self): - """Check if we have seen the opening brace for the innermost block. - - Returns: - True if we have seen the opening brace, False if the innermost - block is still expecting an opening brace. - """ - return (not self.stack) or self.stack[-1].seen_open_brace - - def UpdatePreprocessor(self, line): - """Update preprocessor stack. - - We need to handle preprocessors due to classes like this: - #ifdef SWIG - struct ResultDetailsPageElementExtensionPoint { - #else - struct ResultDetailsPageElementExtensionPoint : public Extension { - #endif - - We make the following assumptions (good enough for most files): - - Preprocessor condition evaluates to true from #if up to first - #else/#elif/#endif. - - - Preprocessor condition evaluates to false from #else/#elif up - to #endif. We still perform lint checks on these lines, but - these do not affect nesting stack. - - Args: - line: current line to check. - """ - if Match(r'^\s*#\s*(if|ifdef|ifndef)\b', line): - # Beginning of #if block, save the nesting stack here. The saved - # stack will allow us to restore the parsing state in the #else - # case. - self.pp_stack.append(_PreprocessorInfo(copy.deepcopy(self.stack))) - elif Match(r'^\s*#\s*(else|elif)\b', line): - # Beginning of #else block - if self.pp_stack: - if not self.pp_stack[-1].seen_else: - # This is the first #else or #elif block. Remember the - # whole nesting stack up to this point. This is what we - # keep after the #endif. - self.pp_stack[-1].seen_else = True - self.pp_stack[-1].stack_before_else = copy.deepcopy( - self.stack) - - # Restore the stack to how it was before the #if - self.stack = copy.deepcopy(self.pp_stack[-1].stack_before_if) - else: - # TODO(unknown): unexpected #else, issue warning? - pass - elif Match(r'^\s*#\s*endif\b', line): - # End of #if or #else blocks. - if self.pp_stack: - # If we saw an #else, we will need to restore the nesting - # stack to its former state before the #else, otherwise we - # will just continue from where we left off. - if self.pp_stack[-1].seen_else: - # Here we can just use a shallow copy since we are the last - # reference to it. - self.stack = self.pp_stack[-1].stack_before_else - # Drop the corresponding #if - self.pp_stack.pop() - else: - # TODO(unknown): unexpected #endif, issue warning? - pass - - def Update(self, clean_lines, linenum): - """Update nesting state with current line. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - """ - line = clean_lines.elided[linenum] - - # Update pp_stack first - self.UpdatePreprocessor(line) - - # Count parentheses. This is to avoid adding struct arguments to - # the nesting stack. - if self.stack: - inner_block = self.stack[-1] - depth_change = line.count('(') - line.count(')') - inner_block.open_parentheses += depth_change - - # Also check if we are starting or ending an inline assembly block. - if inner_block.inline_asm in (_NO_ASM, _END_ASM): - if (depth_change != 0 and - inner_block.open_parentheses == 1 and - _MATCH_ASM.match(line)): - # Enter assembly block - inner_block.inline_asm = _INSIDE_ASM - else: - # Not entering assembly block. If previous line was - # _END_ASM, we will now shift to _NO_ASM state. - inner_block.inline_asm = _NO_ASM - elif (inner_block.inline_asm == _INSIDE_ASM and - inner_block.open_parentheses == 0): - # Exit assembly block - inner_block.inline_asm = _END_ASM - - # Consume braces or semicolons from what's left of the line - while True: - # Match first brace, semicolon, or closed parenthesis. - matched = Match(r'^[^{;)}]*([{;)}])(.*)$', line) - if not matched: - break - - token = matched.group(1) - if token == '{': - # If namespace or class hasn't seen an opening brace yet, mark - # namespace/class head as complete. Push a new block onto the - # stack otherwise. - if not self.SeenOpenBrace(): - self.stack[-1].seen_open_brace = True - else: - self.stack.append(_BlockInfo(True)) - if _MATCH_ASM.match(line): - self.stack[-1].inline_asm = _BLOCK_ASM - elif token == ';' or token == ')': - # If we haven't seen an opening brace yet, but we already saw - # a semicolon, this is probably a forward declaration. Pop - # the stack for these. - # - # Similarly, if we haven't seen an opening brace yet, but we - # already saw a closing parenthesis, then these are probably - # function arguments with extra "class" or "struct" keywords. - # Also pop these stack for these. - if not self.SeenOpenBrace(): - self.stack.pop() - else: # token == '}' - # Perform end of block checks and pop the stack. - if self.stack: - self.stack.pop() - line = matched.group(2) - - -def CheckForNonStandardConstructs(filename, clean_lines, linenum, error): - r"""Logs an error if we see certain non-ANSI constructs ignored by gcc-2. - - Complain about several constructs which gcc-2 accepts, but which are - not standard C++. Warning about these in lint is one way to ease the - transition to new compilers. - - put storage class first (e.g. "static const" instead of "const static"). - - "%" PRId64 instead of %qd" in printf-type functions. - - "%1$d" is non-standard in printf-type functions. - - "\%" is an undefined character escape sequence. - - text after #endif is not allowed. - - invalid inner-style forward declaration. - - >? and ?= and 1: - error(filename, linenum, 'whitespace/todo', 2, - 'Too many spaces before TODO') - - username = match.group(2) - if not username: - return - - colon = match.group(3) - if not colon: - error(filename, linenum, 'readability/todo', 2, - 'Missing colon in TODO; it should look like ' - '"// TODO(my_username): Stuff."') - - middle_whitespace = match.group(4) - # Comparisons made explicit for correctness -- pylint: - # disable=g-explicit-bool-comparison - if middle_whitespace != ' ' and middle_whitespace != '': - error(filename, linenum, 'whitespace/todo', 2, - 'TODO(my_username): should be followed by a space') - - -def FindNextMatchingAngleBracket(clean_lines, linenum, init_suffix): - """Find the corresponding > to close a template. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: Current line number. - init_suffix: Remainder of the current line after the initial <. - - Returns: - True if a matching bracket exists. - """ - line = init_suffix - nesting_stack = ['<'] - while True: - # Find the next operator that can tell us whether < is used as an - # opening bracket or as a less-than operator. We only want to - # warn on the latter case. - # - # We could also check all other operators and terminate the search - # early, e.g. if we got something like this "a(),;\[\]]*([<>(),;\[\]])(.*)$', line) - if match: - # Found an operator, update nesting stack - operator = match.group(1) - line = match.group(2) - - if nesting_stack[-1] == '<': - # Expecting closing angle bracket - if operator in ('<', '(', '['): - nesting_stack.append(operator) - elif operator == '>': - nesting_stack.pop() - if not nesting_stack: - # Found matching angle bracket - return True - elif operator == ',': - # Got a comma after a bracket, this is most likely a - # template argument. We have not seen a closing angle - # bracket yet, but it's probably a few lines later if we - # look for it, so just return early here. - return True - else: - # Got some other operator. - return False - - else: - # Expecting closing parenthesis or closing bracket - if operator in ('<', '(', '['): - nesting_stack.append(operator) - elif operator in (')', ']'): - # We don't bother checking for matching () or []. If we got - # something like (] or [), it would have been a syntax - # error. - nesting_stack.pop() - - else: - # Scan the next line - linenum += 1 - if linenum >= len(clean_lines.elided): - break - line = clean_lines.elided[linenum] - - # Exhausted all remaining lines and still no matching angle bracket. - # Most likely the input was incomplete, otherwise we should have - # seen a semicolon and returned early. - return True - - -def FindPreviousMatchingAngleBracket(clean_lines, linenum, init_prefix): - """Find the corresponding < that started a template. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: Current line number. - init_prefix: Part of the current line before the initial >. - - Returns: - True if a matching bracket exists. - """ - line = init_prefix - nesting_stack = ['>'] - while True: - # Find the previous operator - match = Search(r'^(.*)([<>(),;\[\]])[^<>(),;\[\]]*$', line) - if match: - # Found an operator, update nesting stack - operator = match.group(2) - line = match.group(1) - - if nesting_stack[-1] == '>': - # Expecting opening angle bracket - if operator in ('>', ')', ']'): - nesting_stack.append(operator) - elif operator == '<': - nesting_stack.pop() - if not nesting_stack: - # Found matching angle bracket - return True - elif operator == ',': - # Got a comma before a bracket, this is most likely a - # template argument. The opening angle bracket is probably - # there if we look for it, so just return early here. - return True - else: - # Got some other operator. - return False - - else: - # Expecting opening parenthesis or opening bracket - if operator in ('>', ')', ']'): - nesting_stack.append(operator) - elif operator in ('(', '['): - nesting_stack.pop() - - else: - # Scan the previous line - linenum -= 1 - if linenum < 0: - break - line = clean_lines.elided[linenum] - - # Exhausted all earlier lines and still no matching angle bracket. - return False - -def CheckSpacing(filename, clean_lines, linenum, error): - """Checks for the correctness of various spacing issues in the code. - - Things we check for: spaces around operators, spaces after - if/for/while/switch, no spaces around parens in function calls, two - spaces between code and comment, don't start a block with a blank - line, don't end a function with a blank line, don't add a blank line - after public/protected/private, don't have too many blank lines in a row, - spaces after {, spaces before }. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - - # Don't use "elided" lines here, otherwise we can't check commented lines. - # Don't want to use "raw" either, because we don't want to check inside - # C++11 raw strings, - raw = clean_lines.lines_without_raw_strings - line = raw[linenum] - - # Before nixing comments, check if the line is blank for no good - # reason. This includes the first line after a block is opened, and - # blank lines at the end of a function (ie, right before a line like '}' - # - # Skip all the blank line checks if we are immediately inside a - # namespace body. In other words, don't issue blank line warnings - # for this block: - # namespace { - # - # } - # - # A warning about missing end of namespace comments will be issued instead. - if IsBlankLine(line): - elided = clean_lines.elided - prev_line = elided[linenum - 1] - prevbrace = prev_line.rfind('{') - # TODO(unknown): Don't complain if line before blank line, and line - # after,both start with alnums and are indented the same - # amount. This ignores whitespace at the start of a - # namespace block because those are not usually indented. - if prevbrace != -1 and prev_line[prevbrace:].find('}') == -1: - # OK, we have a blank line at the start of a code block. Before we - # complain, we check if it is an exception to the rule: The previous - # non-empty line has the parameters of a function header that are - # indented 4 spaces (because they did not fit in a 80 column line - # when placed on the same line as the function name). We also check - # for the case where the previous line is indented 6 spaces, which - # may happen when the initializers of a constructor do not fit into - # a 80 column line. - if Match(r' {6}\w', prev_line): # Initializer list? - # We are looking for the opening column of initializer list, - # which should be indented 4 spaces to cause 6 space indentation - # afterwards. - search_position = linenum - 2 - while (search_position >= 0 - and Match(r' {6}\w', elided[search_position])): - search_position -= 1 - - # Next, we complain if there's a comment too near the text - commentpos = line.find('//') - if commentpos != -1: - # Check if the // may be in quotes. If so, ignore it - # Comparisons made explicit for clarity -- pylint: - # disable=g-explicit-bool-comparison - if (line.count('"', 0, commentpos) - - line.count('\\"', 0, commentpos)) % 2 == 0: # not in quotes - # Allow one space for new scopes, two spaces otherwise: - if (not Match(r'^\s*{ //', line) and - ((commentpos >= 1 and - line[commentpos - 1] not in string.whitespace) or - (commentpos >= 2 and - line[commentpos - 2] not in string.whitespace))): - return - # There should always be a space between the // and the comment - commentend = commentpos + 2 - if commentend < len(line) and not line[commentend] == ' ': - # but some lines are exceptions -- e.g. if they're big - # comment delimiters like: - # //---------------------------------------------------------- - # or are an empty C++ style Doxygen comment, like: - # /// - # or C++ style Doxygen comments placed after the variable: - # ///< Header comment - # //!< Header comment - # or they begin with multiple slashes followed by a space: - # //////// Header comment - # or they are Vim {{{ fold markers - match = (Search(r'[=/-]{4,}\s*$', line[commentend:]) or - Search(r'^/$', line[commentend:]) or - Search(r'^!< ', line[commentend:]) or - Search(r'^/< ', line[commentend:]) or - Search(r'^/+ ', line[commentend:]) or - Search(r'^(?:\{{3}|\}{3})\d*(?: |$)', - line[commentend:])) - if not match: - error(filename, linenum, 'whitespace/comments', 4, - 'Should have a space between // and comment') - CheckComment(line[commentpos:], filename, linenum, error) - - line = clean_lines.elided[linenum] # get rid of comments and strings - - # Don't try to do spacing checks for operator methods - line = re.sub(r'operator(==|!=|<|<<|<=|>=|>>|>)\(', r'operator\(', line) - - # We allow no-spaces around = within an if: "if ( (a=Foo()) == 0 )". - # Otherwise not. Note we only check for non-spaces on *both* sides; - # sometimes people put non-spaces on one side when aligning ='s among - # many lines (not that this is behavior that I approve of...) - if Search(r'[\w.]=[\w.]', line) and not Search(r'\b(if|while) ', line): - return - - # It's ok not to have spaces around binary operators like + - * /, but if - # there's too little whitespace, we get concerned. It's hard to tell, - # though, so we punt on this one for now. TODO. - - match = Search(r'(?:[^ (*/![])+(?= first to avoid false positives with < and >, then - # check non-include lines for spacing around < and >. - match = Search(r'[^<>=!\s](==|!=|<=|>=)[^<>=!\s]', line) - if match: - return - - # Boolean operators should be placed on the next line. - if Search(r'(?:&&|\|\|)$', line): - return - - # We allow no-spaces around << when used like this: 10<<20, but - # not otherwise (particularly, not when used as streams) - # Also ignore using ns::operator<<; - match = Search(r'(operator|\S)(?:L|UL|ULL|l|ul|ull)?<<(\S)', line) - if (match and - not (match.group(1).isdigit() and match.group(2).isdigit()) and - not (match.group(1) == 'operator' and match.group(2) == ';')): - error(filename, linenum, 'whitespace/operators', 3, - 'Missing spaces around <<') - elif not Match(r'#.*include', line): - # Avoid false positives on -> - reduced_line = line.replace('->', '') - - # Look for < that is not surrounded by spaces. This is only - # triggered if both sides are missing spaces, even though - # technically should flag if at least one side is missing a - # space. This is done to avoid some false positives with shifts. - match = Search(r'[^\s<]<([^\s=<].*)', reduced_line) - if (match and not FindNextMatchingAngleBracket(clean_lines, linenum, - match.group(1))): - return - - # Look for > that is not surrounded by spaces. Similar to the - # above, we only trigger if both sides are missing spaces to avoid - # false positives with shifts. - match = Search(r'^(.*[^\s>])>[^\s=>]', reduced_line) - if (match and - not FindPreviousMatchingAngleBracket(clean_lines, linenum, - match.group(1))): - return - - # We allow no-spaces around >> for almost anything. This is because - # C++11 allows ">>" to close nested templates, which accounts for - # most cases when ">>" is not followed by a space. - # - # We still warn on ">>" followed by alpha character, because that is - # likely due to ">>" being used for right shifts, e.g.: - # value >> alpha - # - # When ">>" is used to close templates, the alphanumeric letter that - # follows would be part of an identifier, and there should still be - # a space separating the template type and the identifier. - # type> alpha - match = Search(r'>>[a-zA-Z_]', line) - if match: - error(filename, linenum, 'whitespace/operators', 3, - 'Missing spaces around >>') - - # There shouldn't be space around unary operators - match = Search(r'(!\s|~\s|[\s]--[\s;]|[\s]\+\+[\s;])', line) - if match: - return - - # For if/for/while/switch, the left and right parens should be - # consistent about how many spaces are inside the parens, and - # there should either be zero or one spaces inside the parens. - # We don't want: "if ( foo)" or "if ( foo )". - # Exception: "for ( ; foo; bar)" and "for (foo; bar; )" are allowed. - match = Search(r'\b(if|for|while|switch)\s*' - r'\(([ ]*)(.).*[^ ]+([ ]*)\)\s*{\s*$', - line) - if match: - if len(match.group(2)) != len(match.group(4)): - if not (match.group(3) == ';' and - len(match.group(2)) == 1 + len(match.group(4)) or - not match.group(2) and Search(r'\bfor\s*\(.*; \)', line)): - return - if len(match.group(2)) not in [0, 1]: - return - - # Check whether everything inside expressions is aligned correctly - if any(line.find(k) >= 0 for k in BRACES if k != '{'): - return - - # Except after an opening paren, or after another opening brace (in case of - # an initializer list, for instance), you should have spaces before your - # braces. And since you should never have braces at the beginning of a line, - # this is an easy test. - match = Match(r'^(.*[^ ({]){', line) - - # Make sure '} else {' has spaces. - if Search(r'}else', line): - return - - # You shouldn't have spaces before your brackets, except maybe after - # 'delete []' or 'new char * []'. - if Search(r'\w\s+\[', line): - return - - if Search(r'\{(?!\})\S', line): - return - if Search(r'\S(?= 0: - prevline = clean_lines.elided[prevlinenum] - if not IsBlankLine(prevline): # if not a blank line... - return (prevline, prevlinenum) - prevlinenum -= 1 - return ('', -1) - - -def CheckBraces(filename, clean_lines, linenum, error): - """Looks for misplaced braces (e.g. at the end of line). - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - - line = clean_lines.elided[linenum] # get rid of comments and strings - - if Match(r'\s+{\s*$', line): - # We allow an open brace to start a line in the case where someone - # is using braces in a block to explicitly create a new scope, which - # is commonly used to control the lifetime of stack-allocated - # variables. Braces are also used for brace initializers inside - # function calls. We don't detect this perfectly: we just don't - # complain if the last non-whitespace character on the previous - # non-blank line is ',', ';', ':', '(', '{', or '}', or if the - # previous line starts a preprocessor block. - prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0] - if (not Search(r'[,;:}{(]\s*$', prevline) and - not Match(r'\s*#', prevline)): - return - - # Brace must appear after function signature, but on the *next* line - if Match(r'^(?:\w+(?: ?\*+)? )+\w+\(', line): - pos = line.find('(') - (endline, end_linenum, _) = CloseExpression(clean_lines, linenum, pos) - if endline.endswith('{'): - return - - func_start_linenum = end_linenum + 1 - while not clean_lines.lines[func_start_linenum] == "{": - attrline = Match( - r'^((?!# *define).*?)' - r'(?:FUNC_ATTR|FUNC_API|REAL_FATTR)_\w+' - r'(?:\(\d+(, \d+)*\))?', - clean_lines.lines[func_start_linenum], - ) - if attrline: - if len(attrline.group(1)) != 2: - error(filename, func_start_linenum, - 'whitespace/indent', 5, - 'Function attribute line should have 2-space ' - 'indent') - - func_start_linenum += 1 - else: - func_start = clean_lines.lines[func_start_linenum] - if not func_start.startswith('enum ') and func_start.endswith('{'): - return - break - -def CheckStyle(filename, clean_lines, linenum, error): - """Checks rules from the 'C++ style rules' section of cppguide.html. - - Most of these rules are hard to test (naming, comment style), but we - do what we can. In particular we check for 2-space indents, line lengths, - tab usage, spaces inside code, etc. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - - # Some more style checks - CheckBraces(filename, clean_lines, linenum, error) - CheckSpacing(filename, clean_lines, linenum, error) - - -_RE_PATTERN_INCLUDE = re.compile(r'^\s*#\s*include\s*([<"])([^>"]*)[>"].*$') - - -def _GetTextInside(text, start_pattern): - r"""Retrieves all the text between matching open and close parentheses. - - Given a string of lines and a regular expression string, retrieve all the - text following the expression and between opening punctuation symbols like - (, [, or {, and the matching close-punctuation symbol. This properly nested - occurrences of the punctuations, so for the text like - printf(a(), b(c())); - a call to _GetTextInside(text, r'printf\(') will return 'a(), b(c())'. - start_pattern must match string having an open punctuation symbol at the - end. - - Args: - text: The lines to extract text. Its comments and strings must be elided. - It can be single line and can span multiple lines. - start_pattern: The regexp string indicating where to start extracting - the text. - Returns: - The extracted text. - None if either the opening string or ending punctuation couldn't be found. - """ - # TODO(sugawarayu): Audit cpplint.py to see what places could be profitably - # rewritten to use _GetTextInside (and use inferior regexp matching today). - - # Give opening punctuations to get the matching close-punctuations. - matching_punctuation = {'(': ')', '{': '}', '[': ']'} - closing_punctuation = set(matching_punctuation.values()) - - # Find the position to start extracting text. - match = re.search(start_pattern, text, re.M) - if not match: # start_pattern not found in text. - return None - start_position = match.end(0) - - assert start_position > 0, ( - 'start_pattern must ends with an opening punctuation.') - assert text[start_position - 1] in matching_punctuation, ( - 'start_pattern must ends with an opening punctuation.') - # Stack of closing punctuations we expect to have in text after position. - punctuation_stack = [matching_punctuation[text[start_position - 1]]] - position = start_position - while punctuation_stack and position < len(text): - if text[position] == punctuation_stack[-1]: - punctuation_stack.pop() - elif text[position] in closing_punctuation: - # A closing punctuation without matching opening punctuations. - return None - elif text[position] in matching_punctuation: - punctuation_stack.append(matching_punctuation[text[position]]) - position += 1 - if punctuation_stack: - # Opening punctuations left without matching close-punctuations. - return None - # punctuations match. - return text[start_position:position - 1] - - -def CheckLanguage(filename, clean_lines, linenum, error): - """Checks rules from the 'C++ language rules' section of cppguide.html. - - Some of these rules are hard to test (function overloading, using - uint32 inappropriately), but we do the best we can. - - Args: - filename : The name of the current file. - clean_lines : A CleansedLines instance containing the file. - linenum : The number of the line to check. - error : The function to call with any errors found. - """ - # If the line is empty or consists of entirely a comment, no need to - # check it. - line = clean_lines.elided[linenum] - if not line: - return - - # TODO(unknown): figure out if they're using default arguments in fn proto. - - # Check if people are using the verboten C basic types. - match = Search(r'\b(short|long long)\b', line) - if match: - error(filename, linenum, 'runtime/int', 4, - 'Use int16_t/int64_t/etc, rather than the C type %s' - % match.group(1)) - - # When snprintf is used, the second argument shouldn't be a literal. - match = Search(r'snprintf\s*\(([^,]*),\s*([0-9]*)\s*,', line) - if match and match.group(2) != '0': - # If 2nd arg is zero, snprintf is used to calculate size. - error(filename, linenum, 'runtime/printf', 3, - 'If you can, use sizeof(%s) instead of %s as the 2nd arg ' - 'to snprintf.' % (match.group(1), match.group(2))) - - # Check if some verboten C functions are being used. - if Search(r'\bsprintf\b', line): - error(filename, linenum, 'runtime/printf', 5, - 'Use snprintf instead of sprintf.') - match = Search(r'\b(strncpy|STRNCPY)\b', line) - if match: - error(filename, linenum, 'runtime/printf', 4, - 'Use xstrlcpy, xmemcpyz or snprintf instead of %s (unless this is from Vim)' - % match.group(1)) - match = Search(r'\b(strcpy)\b', line) - if match: - error(filename, linenum, 'runtime/printf', 4, - 'Use xstrlcpy, xmemcpyz or snprintf instead of %s' % match.group(1)) - match = Search(r'\b(STRNCAT|strncat|vim_strcat)\b', line) - if match: - error(filename, linenum, 'runtime/printf', 4, - 'Use xstrlcat or snprintf instead of %s' % match.group(1)) - if not Search(r'eval/typval\.[ch]$|eval/typval_defs\.h$', filename): - match = Search(r'(?:\.|->)' - r'(?:lv_(?:first|last|refcount|len|watch|idx(?:_item)?' - r'|copylist|lock)' - r'|li_(?:next|prev|tv))\b', line) - if match: - error(filename, linenum, 'runtime/deprecated', 4, - 'Accessing list_T internals directly is prohibited; ' - 'see https://neovim.io/doc/user/dev_vimpatch.html#dev-vimpatch-list-management') - - # Check for suspicious usage of "if" like - # } if (a == b) { - if Search(r'\}\s*if\s*\(', line): - return - - # Check for potential format string bugs like printf(foo). - # We constrain the pattern not to pick things like DocidForPrintf(foo). - # Not perfect but it can catch printf(foo.c_str()) and printf(foo->c_str()) - # TODO(sugawarayu): Catch the following case. Need to change the calling - # convention of the whole function to process multiple line to handle it. - # printf( - # boy_this_is_a_really_long_variable_that_cannot_fit_on_the_prev_line); - printf_args = _GetTextInside(line, r'(?i)\b(string)?printf\s*\(') - if printf_args: - match = Match(r'([\w.\->()]+)$', printf_args) - if match and match.group(1) != '__VA_ARGS__': - function_name_groups = re.search(r'\b((?:string)?printf)\s*\(', line, re.I) - assert function_name_groups - function_name = function_name_groups.group(1) - error(filename, linenum, 'runtime/printf', 4, - 'Potential format string bug. Do %s("%%s", %s) instead.' - % (function_name, match.group(1))) - - # Check for potential memset bugs like memset(buf, sizeof(buf), 0). - match = Search(r'memset\s*\(([^,]*),\s*([^,]*),\s*0\s*\)', line) - if match and not Match(r"^''|-?[0-9]+|0x[0-9A-Fa-f]$", match.group(2)): - error(filename, linenum, 'runtime/memset', 4, - 'Did you mean "memset(%s, 0, %s)"?' - % (match.group(1), match.group(2))) - - # Detect variable-length arrays. - match = Match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line) - if (match and match.group(2) != 'return' and match.group(2) != 'delete' and - match.group(3).find(']') == -1): - # Split the size using space and arithmetic operators as delimiters. - # If any of the resulting tokens are not compile time constants then - # report the error. - tokens = re.split(r'\s|\+|\-|\*|\/|<<|>>]', match.group(3)) - is_const = True - skip_next = False - for tok in tokens: - if skip_next: - skip_next = False - continue - - if Search(r'sizeof\(.+\)', tok): - continue - if Search(r'arraysize\(\w+\)', tok): - continue - - tok = tok.lstrip('(') - tok = tok.rstrip(')') - if not tok: - continue - if Match(r'\d+', tok): - continue - if Match(r'0[xX][0-9a-fA-F]+', tok): - continue - if Match(r'k[A-Z0-9]\w*', tok): - continue - if Match(r'(.+::)?k[A-Z0-9]\w*', tok): - continue - if Match(r'(.+::)?[A-Z][A-Z0-9_]*', tok): - continue - # A catch all for tricky sizeof cases, including - # 'sizeof expression', 'sizeof(*type)', 'sizeof(const type)', - # 'sizeof(struct StructName)' requires skipping the next token - # because we split on ' ' and '*'. - if tok.startswith('sizeof'): - skip_next = True - continue - is_const = False - break - if not is_const: - error(filename, linenum, 'runtime/arrays', 1, - "Do not use variable-length arrays. Use an appropriately" - " named ('k' followed by CamelCase) compile-time constant for" - " the size.") - - # INIT() macro should only be used in header files. - if not filename.endswith('.h') and Search(r' INIT\(', line): - error(filename, linenum, 'build/init_macro', 4, - 'INIT() macro should only be used in header files.') - - # Detect TRUE and FALSE. - match = Search(r'\b(TRUE|FALSE)\b', line) - if match: - token = match.group(1) - error(filename, linenum, 'readability/bool', 4, - 'Use {} instead of {}.'.format(token.lower(), token)) - - # Detect MAYBE - match = Search(r'\b(MAYBE)\b', line) - if match: - token = match.group(1) - error(filename, linenum, 'readability/bool', 4, - 'Use kNONE from TriState instead of %s.' % token) - - # Detect preincrement/predecrement - match = Match(r'^\s*(?:\+\+|--)', line) - if match: - error(filename, linenum, 'readability/increment', 5, - 'Do not use preincrement in statements, ' - 'use postincrement instead') - # Detect preincrement/predecrement in for(;; preincrement) - match = Search(r';\s*(\+\+|--)', line) - if match: - end_pos, end_depth = FindEndOfExpressionInLine(line, match.start(1), 1, - '(', ')') - expr = line[match.start(1):end_pos] - if end_depth == 0 and ';' not in expr and ' = ' not in expr: - error(filename, linenum, 'readability/increment', 4, - 'Do not use preincrement in statements, including ' - 'for(;; action)') - - -def ProcessLine(filename, clean_lines, line, - nesting_state, error, - extra_check_functions=[]): - """Processes a single line in the file. - - Args: - filename : Filename of the file that is being processed. - clean_lines : An array of strings, each representing a line of - the file, with comments stripped. - line : Number of line being processed. - nesting_state : A _NestingState instance which maintains - information about the current stack of nested - blocks being parsed. - error : A callable to which errors are reported, which - takes 4 arguments: filename, line number, error - level, and message - extra_check_functions : An array of additional check functions that will - be run on each source line. Each function takes 4 - arguments : filename, clean_lines, line, error - """ - raw_lines = clean_lines.raw_lines - init_lines = clean_lines.init_lines - ParseNolintSuppressions(raw_lines[line], line) - nesting_state.Update(clean_lines, line) - if nesting_state.stack and nesting_state.stack[-1].inline_asm != _NO_ASM: - return - CheckForMultilineCommentsAndStrings(filename, clean_lines, line, error) - CheckForOldStyleComments(filename, init_lines[line], line, error) - CheckStyle(filename, clean_lines, line, error) - CheckLanguage(filename, clean_lines, line, error) - CheckForNonStandardConstructs(filename, clean_lines, line, error) - CheckPosixThreading(filename, clean_lines, line, error) - CheckMemoryFunctions(filename, clean_lines, line, error) - CheckOSFunctions(filename, clean_lines, line, error) - for check_fn in extra_check_functions: - check_fn(filename, clean_lines, line, error) - - -def ProcessFileData(filename, file_extension, lines, error, - extra_check_functions=[]): - """Performs lint checks and reports any errors to the given error function. - - Args: - filename: Filename of the file that is being processed. - file_extension: The extension (dot not included) of the file. - lines: An array of strings, each representing a line of the file, with the - last element being empty if the file is terminated with a newline. - error: A callable to which errors are reported, which takes 4 arguments: - filename, line number, error level, and message - extra_check_functions: An array of additional check functions that will be - run on each source line. Each function takes 4 - arguments: filename, clean_lines, line, error - """ - lines = (['// marker so line numbers and indices both start at 1'] + lines + - ['// marker so line numbers end in a known way']) - - nesting_state = _NestingState() - - ResetNolintSuppressions() - ResetKnownErrorSuppressions() - - for line in range(1, len(lines)): - ParseKnownErrorSuppressions(filename, lines, line) - - init_lines = lines[:] - - if _cpplint_state.record_errors_file: - def RecordedError(filename, linenum, category, confidence, message): - if not IsErrorSuppressedByNolint(category, linenum): - key = init_lines[linenum - 1 if linenum else 0:linenum + 2] - err = [filename, key, category] - assert _cpplint_state.record_errors_file - json.dump(err, _cpplint_state.record_errors_file) - _cpplint_state.record_errors_file.write('\n') - Error(filename, linenum, category, confidence, message) - - error = RecordedError - - RemoveMultiLineComments(filename, lines, error) - clean_lines = CleansedLines(lines, init_lines) - for line in range(clean_lines.NumLines()): - ProcessLine(filename, clean_lines, line, - nesting_state, error, - extra_check_functions) - - if file_extension == 'h': - CheckForHeaderGuard(filename, lines, error) - CheckIncludes(filename, lines, error) - if filename.endswith('/defs.h') or filename.endswith('_defs.h'): - CheckNonSymbols(filename, lines, error) - - # We check here rather than inside ProcessLine so that we see raw - # lines rather than "cleaned" lines. - CheckForBadCharacters(filename, lines, error) - - -def ProcessFile(filename, vlevel, extra_check_functions=[]): - """Does neovim-lint on a single file. - - Args: - filename: The name of the file to parse. - - vlevel: The level of errors to report. Every error of confidence - >= verbose_level will be reported. 0 is a good default. - - extra_check_functions: An array of additional check functions that will be - run on each source line. Each function takes 4 - arguments: filename, clean_lines, line, error - """ - - _SetVerboseLevel(vlevel) - - try: - # Support the Unix convention of using "-" for stdin. Note that - # we are not opening the file with universal newline support - # (which codecs doesn't support anyway), so the resulting lines do - # contain trailing '\r' characters if we are reading a file that - # has CRLF endings. - # If after the split a trailing '\r' is present, it is removed - # below. If it is not expected to be present (i.e. os.linesep != - # '\r\n' as in Windows), a warning is issued below if this file - # is processed. - - if filename == '-': - stdin = sys.stdin.read() - lines = stdin.split('\n') - if _cpplint_state.stdin_filename is not None: - filename = _cpplint_state.stdin_filename - else: - lines = open( - filename, 'r', encoding='utf-8', errors='replace', newline=None).read().split('\n') - - # Remove trailing '\r'. - for linenum in range(len(lines)): - if lines[linenum].endswith('\r'): - lines[linenum] = lines[linenum].rstrip('\r') - - except OSError: - sys.stderr.write( - "Skipping input '%s': Can't open for reading\n" % filename) - return - - # Note, if no dot is found, this will give the entire filename as the ext. - file_extension = filename[filename.rfind('.') + 1:] - - # When reading from stdin, the extension is unknown, so no cpplint tests - # should rely on the extension. - if filename != '-' and file_extension not in _valid_extensions: - sys.stderr.write('Ignoring {}; only linting {} files\n'.format( - filename, - ', '.join('.{}'.format(ext) for ext in _valid_extensions))) - else: - ProcessFileData(filename, file_extension, lines, Error, - extra_check_functions) - -def PrintUsage(message): - """Prints a brief usage string and exits, optionally with an error message. - - Args: - message: The optional error message. - """ - if message: - sys.stderr.write(_USAGE) - sys.exit('\nFATAL ERROR: ' + message) - else: - sys.stdout.write(_USAGE) - sys.exit(0) - - -def PrintCategories(): - """Prints a list of all the error-categories used by error messages. - - These are the categories used to filter messages via --filter. - """ - sys.stdout.write(''.join(' %s\n' % cat for cat in _ERROR_CATEGORIES)) - sys.exit(0) - - -def ParseArguments(args): - """Parses the command line arguments. - - This may set the output format and verbosity level as side-effects. - - Args: - args: The command line arguments: - - Returns: - The list of filenames to lint. - """ - opts = [] - filenames = [] - try: - (opts, filenames) = getopt.getopt(args, '', ['help', - 'output=', - 'verbose=', - 'counting=', - 'filter=', - 'root=', - 'linelength=', - 'extensions=', - 'record-errors=', - 'suppress-errors=', - 'stdin-filename=', - ]) - except getopt.GetoptError: - PrintUsage('Invalid arguments.') - - verbosity = _VerboseLevel() - output_format = _OutputFormat() - filters = '' - counting_style = '' - record_errors_file = None - suppress_errors_file = None - stdin_filename = '' - - for (opt, val) in opts: - if opt == '--help': - PrintUsage(None) - elif opt == '--output': - if val not in ('emacs', 'vs7', 'eclipse', 'gh_action'): - PrintUsage('The only allowed output formats are emacs,' - ' vs7 and eclipse.') - output_format = val - elif opt == '--verbose': - verbosity = int(val) - elif opt == '--filter': - filters = val - if not filters: - PrintCategories() - elif opt == '--counting': - if val not in ('total', 'toplevel', 'detailed'): - PrintUsage( - 'Valid counting options are total, toplevel, and detailed') - counting_style = val - elif opt == '--linelength': - global _line_length - try: - _line_length = int(val) - except ValueError: - PrintUsage('Line length must be digits.') - elif opt == '--extensions': - global _valid_extensions - try: - _valid_extensions = set(val.split(',')) - except ValueError: - PrintUsage('Extensions must be comma separated list.') - elif opt == '--record-errors': - record_errors_file = val - elif opt == '--suppress-errors': - suppress_errors_file = val - elif opt == '--stdin-filename': - stdin_filename = val - - if not filenames: - PrintUsage('No files were specified.') - - _SetOutputFormat(output_format) - _SetVerboseLevel(verbosity) - _SetFilters(filters) - _SetCountingStyle(counting_style) - _SuppressErrorsFrom(suppress_errors_file) - _RecordErrorsTo(record_errors_file) - _cpplint_state.stdin_filename = stdin_filename - - return filenames - - -def main(): - filenames = ParseArguments(sys.argv[1:]) - - _cpplint_state.ResetErrorCounts() - for filename in filenames: - ProcessFile(filename, _cpplint_state.verbose_level) - _cpplint_state.PrintErrorCounts() - - sys.exit(_cpplint_state.error_count > 0) - - -if __name__ == '__main__': - main() - -# vim: ts=4 sts=4 sw=4 foldmarker=▶,▲ - -# Ignore "too complex" warnings when using pymode. -# pylama:ignore=C901 diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 408d1204c3..035e232780 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -928,12 +928,13 @@ else() endif() add_glob_target( TARGET lintc-clint - COMMAND ${PROJECT_SOURCE_DIR}/src/clint.py - FLAGS --output=${LINT_OUTPUT_FORMAT} + COMMAND $ + FLAGS --clean -l ${PROJECT_SOURCE_DIR}/src/clint.lua --output=${LINT_OUTPUT_FORMAT} FILES ${LINT_NVIM_SOURCES} EXCLUDE tui/terminfo_defs.h xxd/xxd.c) +add_dependencies(lintc-clint nvim_bin) set(UNCRUSTIFY_PRG ${DEPS_BIN_DIR}/uncrustify) set(UNCRUSTIFY_CONFIG ${PROJECT_SOURCE_DIR}/src/uncrustify.cfg) diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 7da9a41462..e40cf17086 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -116,8 +116,8 @@ bool tv_in_free_unref_items = false; const char *const tv_empty_string = ""; -//{{{1 Lists -//{{{2 List item +// Lists: +// List item: /// Allocate a list item /// @@ -150,7 +150,7 @@ listitem_T *tv_list_item_remove(list_T *const l, listitem_T *const item) return next_item; } -//{{{2 List watchers +// List watchers: /// Add a watcher to a list /// @@ -198,7 +198,7 @@ static void tv_list_watch_fix(list_T *const l, const listitem_T *const item) } } -//{{{2 Alloc/free +// Alloc/free: /// Allocate an empty list /// @@ -335,7 +335,7 @@ void tv_list_unref(list_T *const l) } } -//{{{2 Add/remove +// Add/remove: /// Remove items "item" to "item2" from list "l" /// @@ -578,7 +578,7 @@ void tv_list_append_number(list_T *const l, const varnumber_T n) }); } -//{{{2 Operations on the whole list +// Operations on the whole list: /// Make a copy of list /// @@ -1569,7 +1569,7 @@ void tv_list_reverse(list_T *const l) l->lv_idx = l->lv_len - l->lv_idx - 1; } -//{{{2 Indexing/searching +// Indexing/searching: /// Locate item with a given index in a list and return it /// @@ -1719,8 +1719,8 @@ int tv_list_idx_of_item(const list_T *const l, const listitem_T *const item) return -1; } -//{{{1 Dictionaries -//{{{2 Dictionary watchers +// Dictionaries: +// Dictionary watchers: /// Perform all necessary cleanup for a `DictWatcher` instance /// @@ -2014,7 +2014,7 @@ void tv_dict_watcher_notify(dict_T *const dict, const char *const key, typval_T } } -//{{{2 Dictionary item +// Dictionary item: /// Allocate a dictionary item /// @@ -2094,7 +2094,7 @@ void tv_dict_item_remove(dict_T *const dict, dictitem_T *const item) tv_dict_item_free(item); } -//{{{2 Alloc/free +// Alloc/free: /// Allocate an empty dictionary. /// Caller should take care of the reference count. @@ -2204,7 +2204,7 @@ void tv_dict_unref(dict_T *const d) } } -//{{{2 Indexing/searching +// Indexing/searching: /// Find item in dictionary /// @@ -2428,7 +2428,7 @@ int tv_dict_wrong_func_name(dict_T *d, typval_T *tv, const char *name) && var_wrong_func_name(name, true); } -//{{{2 dict_add* +// dict_add*: /// Add item to dictionary /// @@ -2659,7 +2659,7 @@ int tv_dict_add_func(dict_T *const d, const char *const key, const size_t key_le return OK; } -//{{{2 Operations on the whole dict +// Operations on the whole dict: /// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. /// @@ -2868,8 +2868,8 @@ void tv_dict_set_keys_readonly(dict_T *const dict) }); } -//{{{1 Blobs -//{{{2 Alloc/free +// Blobs: +// Alloc/free: /// Allocate an empty blob. /// @@ -2906,7 +2906,7 @@ void tv_blob_unref(blob_T *const b) } } -//{{{2 Operations on the whole blob +// Operations on the whole blob: /// Check whether two blobs are equal. /// @@ -3172,9 +3172,9 @@ void f_list2blob(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) }); } -//{{{1 Generic typval operations -//{{{2 Init/alloc/clear -//{{{3 Alloc +// Generic typval operations: +// Init/alloc/clear: +// Alloc: /// Allocate an empty list for a return value /// @@ -3367,7 +3367,7 @@ void tv_blob_copy(blob_T *const from, typval_T *const to) } } -//{{{3 Clear +// Clear: #define TYPVAL_ENCODE_ALLOW_SPECIALS false #define TYPVAL_ENCODE_CHECK_BEFORE @@ -3634,7 +3634,7 @@ void tv_clear(typval_T *const tv) assert(evn_ret == OK); } -//{{{3 Free +// Free: /// Free allocated Vimscript object and value stored inside /// @@ -3674,7 +3674,7 @@ void tv_free(typval_T *tv) xfree(tv); } -//{{{3 Copy +// Copy: /// Copy typval from one location to another /// @@ -3730,7 +3730,7 @@ void tv_copy(const typval_T *const from, typval_T *const to) } } -//{{{2 Locks +// Locks: /// Lock or unlock an item /// @@ -3909,7 +3909,7 @@ bool value_check_lock(VarLockStatus lock, const char *name, size_t name_len) return true; } -//{{{2 Comparison +// Comparison: static int tv_equal_recurse_limit; @@ -4000,7 +4000,7 @@ bool tv_equal(typval_T *const tv1, typval_T *const tv2, const bool ic) return false; } -//{{{2 Type checks +// Type checks: /// Check that given value is a number or string /// @@ -4137,7 +4137,7 @@ bool tv_check_str(const typval_T *const tv) return false; } -//{{{2 Get +// Get: /// Get the number value of a Vimscript object /// diff --git a/src/uncrustify.cfg b/src/uncrustify.cfg index 0681fd7a54..0a596004e2 100644 --- a/src/uncrustify.cfg +++ b/src/uncrustify.cfg @@ -1028,7 +1028,7 @@ sp_before_for_colon = ignore # ignore/add/remove/force sp_extern_paren = ignore # ignore/add/remove/force # Add or remove space after the opening of a C++ comment, as in '// A'. -sp_cmt_cpp_start = ignore # ignore/add/remove/force +sp_cmt_cpp_start = add # ignore/add/remove/force # remove space after the '//' and the pvs command '-V1234', # only works with sp_cmt_cpp_start set to add or force. diff --git a/test/functional/fixtures/clint_test.c b/test/functional/fixtures/clint_test.c new file mode 100644 index 0000000000..dc1eb1bd87 --- /dev/null +++ b/test/functional/fixtures/clint_test.c @@ -0,0 +1,163 @@ +// Test file to trigger all ERROR_CATEGORIES in clint.lua +// This file contains intentional errors to test the linter + +#include +#include +#include + +// build/endif_comment: Uncommented text after #endif +#ifdef SOME_CONDITION +# define TEST 1 +#endif SOME_CONDITION + +// build/include_defs: Non-defs header included (but this is a .c file, so might not trigger) + +// build/printf_format: %q format specifier +void test_printf_format() +{ + printf("%q", "test"); // Should trigger runtime/printf_format +} + +// build/storage_class: Storage class not first +const static int x = 5; // Should trigger build/storage_class + +// readability/bool: Use TRUE/FALSE instead of true/false +#define TRUE 1 +#define FALSE 0 +#define MAYBE 2 + +void test_bool() +{ + int flag = TRUE; // Should trigger readability/bool + if (flag == FALSE) { // Should trigger readability/bool + printf("false\n"); + } + int maybe_val = MAYBE; // Should trigger readability/bool +} + +// readability/multiline_comment: Complex multi-line comment +void test_multiline_comment() +{ + /* This is a multi-line + comment that spans + multiple lines and doesn't close properly on the same line */ +} + +// readability/nul: NUL byte in file (can't easily test this in text) + +// readability/utf8: Invalid UTF-8 (can't easily test) + +// readability/increment: Pre-increment in statements +void test_increment() +{ + int i = 0; + ++i; // Should trigger readability/increment + for (int j = 0; j < 10; ++j) { // Should trigger readability/increment + printf("%d\n", j); + } +} + +// runtime/arrays: Variable-length arrays +void test_arrays(int size) +{ + int arr[size]; // Should trigger runtime/arrays +} + +// runtime/int: Use C basic types instead of fixed-width +void test_int_types() +{ + short x = 1; // Should trigger runtime/int + long long y = 2; // Should trigger runtime/int +} + +// runtime/memset: memset with wrong arguments +void test_memset() +{ + char buf[100]; + memset(buf, sizeof(buf), 0); // Should trigger runtime/memset +} + +// runtime/printf: Use sprintf instead of snprintf +void test_printf() +{ + char buf[100]; + sprintf(buf, "test"); // Should trigger runtime/printf +} + +// runtime/printf_format: %N$ formats +void test_printf_format2() +{ + printf("%1$d", 42); // Should trigger runtime/printf_format +} + +// runtime/threadsafe_fn: Use non-thread-safe functions +void test_threading() +{ + time_t t; + char *time_str = ctime(&t); // Should trigger runtime/threadsafe_fn + asctime(localtime(&t)); // Should trigger runtime/threadsafe_fn +} + +// runtime/deprecated: (This might be Neovim-specific) + +// whitespace/comments: Missing space after // +void test_comments() +{ + int x = 5; // This is a comment // Should trigger whitespace/comments +} + +// whitespace/indent: (Hard to test in this format) + +// whitespace/operators: (Hard to test) + +// whitespace/cast: (Hard to test) + +// build/init_macro: INIT() macro in non-header (but this is a .c file) + +// build/header_guard: No #pragma once (but this is a .c file) + +// build/defs_header: extern variables in _defs.h (but this is a .c file) + +// readability/old_style_comment: Old-style /* */ comment +void test_old_style_comment() +{ + int x = 5; /* This is an old-style comment */ // Should trigger readability/old_style_comment +} + +// Try to trigger more categories +void test_more() +{ + // Try strcpy and strncpy + char dest[100]; + char src[] = "test"; + strcpy(dest, src); // Should trigger runtime/printf + strncpy(dest, src, sizeof(dest)); // Should trigger runtime/printf + + // Try malloc and free (should trigger runtime/memory_fn) + int *ptr = malloc(sizeof(int)); // Should trigger runtime/memory_fn + free(ptr); // Should trigger runtime/memory_fn + + // Try getenv and setenv + char *env = getenv("HOME"); // Should trigger runtime/os_fn + setenv("TEST", "value", 1); // Should trigger runtime/os_fn +} + +int main() +{ + test_printf_format(); + test_bool(); + test_multiline_comment(); + test_multiline_string(); + test_increment(); + test_arrays(10); + test_int_types(); + test_memset(); + test_printf(); + test_printf_format2(); + test_threading(); + test_comments(); + test_old_style_comment(); + test_more(); + + return 0; +} diff --git a/test/functional/script/clint_spec.lua b/test/functional/script/clint_spec.lua new file mode 100644 index 0000000000..8ead44ad3b --- /dev/null +++ b/test/functional/script/clint_spec.lua @@ -0,0 +1,50 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +describe('clint.lua', function() + local clint_path = 'src/clint.lua' + local test_file = 'test/functional/fixtures/clint_test.c' + + local function run_clint(filepath) + local proc = n.spawn_wait('-l', clint_path, filepath) + local output = proc:output() + local lines = vim.split(output, '\n', { plain = true, trimempty = true }) + return lines + end + + it('a linter lints', function() + local output_lines = run_clint(test_file) + local expected = { + 'test/functional/fixtures/clint_test.c:11: Uncommented text after #endif is non-standard. Use a comment. [build/endif_comment] [5]', + 'test/functional/fixtures/clint_test.c:18: "%q" in format strings is deprecated. Use "%" PRId64 instead. [runtime/printf_format] [3]', + 'test/functional/fixtures/clint_test.c:22: Storage class (static, extern, typedef, etc) should be first. [build/storage_class] [5]', + 'test/functional/fixtures/clint_test.c:25: Use true instead of TRUE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:26: Use false instead of FALSE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:27: Use kNONE from TriState instead of MAYBE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:31: Use true instead of TRUE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:32: Use false instead of FALSE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:35: Use kNONE from TriState instead of MAYBE. [readability/bool] [4]', + 'test/functional/fixtures/clint_test.c:41: /*-style comment found, it should be replaced with //-style. /*-style comments are only allowed inside macros. Note that you should not use /*-style comments to document macros itself, use doxygen-style comments for this. [readability/old_style_comment] [5]', + 'test/functional/fixtures/clint_test.c:54: Do not use preincrement in statements, use postincrement instead [readability/increment] [5]', + 'test/functional/fixtures/clint_test.c:55: Do not use preincrement in statements, including for(;; action) [readability/increment] [4]', + "test/functional/fixtures/clint_test.c:63: Do not use variable-length arrays. Use an appropriately named ('k' followed by CamelCase) compile-time constant for the size. [runtime/arrays] [1]", + 'test/functional/fixtures/clint_test.c:69: Use int16_t/int64_t/etc, rather than the C type short [runtime/int] [4]', + 'test/functional/fixtures/clint_test.c:70: Use int16_t/int64_t/etc, rather than the C type long long [runtime/int] [4]', + 'test/functional/fixtures/clint_test.c:77: Did you mean "memset(buf, 0, sizeof(buf))"? [runtime/memset] [4]', + 'test/functional/fixtures/clint_test.c:84: Use snprintf instead of sprintf. [runtime/printf] [5]', + 'test/functional/fixtures/clint_test.c:90: %N$ formats are unconventional. Try rewriting to avoid them. [runtime/printf_format] [2]', + 'test/functional/fixtures/clint_test.c:97: Use os_ctime_r(...) instead of ctime(...). If it is missing, consider implementing it; see os_localtime_r for an example. [runtime/threadsafe_fn] [2]', + 'test/functional/fixtures/clint_test.c:98: Use os_asctime_r(...) instead of asctime(...). If it is missing, consider implementing it; see os_localtime_r for an example. [runtime/threadsafe_fn] [2]', + 'test/functional/fixtures/clint_test.c:98: Use os_localtime_r(...) instead of localtime(...). If it is missing, consider implementing it; see os_localtime_r for an example. [runtime/threadsafe_fn] [2]', + 'test/functional/fixtures/clint_test.c:124: /*-style comment found, it should be replaced with //-style. /*-style comments are only allowed inside macros. Note that you should not use /*-style comments to document macros itself, use doxygen-style comments for this. [readability/old_style_comment] [5]', + 'test/functional/fixtures/clint_test.c:133: Use xstrlcpy, xmemcpyz or snprintf instead of strcpy [runtime/printf] [4]', + 'test/functional/fixtures/clint_test.c:134: Use xstrlcpy, xmemcpyz or snprintf instead of strncpy (unless this is from Vim) [runtime/printf] [4]', + 'test/functional/fixtures/clint_test.c:137: Use xmalloc(...) instead of malloc(...). [runtime/memory_fn] [2]', + 'test/functional/fixtures/clint_test.c:138: Use xfree(...) instead of free(...). [runtime/memory_fn] [2]', + 'test/functional/fixtures/clint_test.c:141: Use os_getenv(...) instead of getenv(...). [runtime/os_fn] [2]', + 'test/functional/fixtures/clint_test.c:142: Use os_setenv(...) instead of setenv(...). [runtime/os_fn] [2]', + 'Total errors found: 28', + } + t.eq(expected, output_lines) + end) +end)