clint.lua (46955B)
1 #!/usr/bin/env nvim -l 2 3 -- Lints C files in the Neovim source tree. 4 -- Based on Google "cpplint", modified for Neovim. 5 -- 6 -- Test coverage: `test/functional/script/clint_spec.lua` 7 -- 8 -- This can get very confused by /* and // inside strings! We do a small hack, 9 -- which is to ignore //'s with "'s after them on the same line, but it is far 10 -- from perfect (in either direction). 11 12 local vim = vim 13 14 -- Error categories used for filtering 15 local ERROR_CATEGORIES = { 16 'build/endif_comment', 17 'build/header_guard', 18 'build/include_defs', 19 'build/defs_header', 20 'build/printf_format', 21 'build/storage_class', 22 'build/init_macro', 23 'readability/bool', 24 'readability/multiline_comment', 25 -- Dropped 'readability/multiline_string' detection because it is too buggy, and uncommon. 26 -- 'readability/multiline_string', 27 'readability/nul', 28 'readability/utf8', 29 'readability/increment', 30 'runtime/arrays', 31 'runtime/int', 32 'runtime/memset', 33 'runtime/printf', 34 'runtime/printf_format', 35 'runtime/threadsafe_fn', 36 'runtime/deprecated', 37 'whitespace/indent', 38 'whitespace/operators', 39 'whitespace/cast', 40 } 41 42 -- Default filters (empty by default) 43 local DEFAULT_FILTERS = {} 44 45 -- Assembly state constants 46 local NO_ASM = 0 -- Outside of inline assembly block 47 local INSIDE_ASM = 1 -- Inside inline assembly block 48 local END_ASM = 2 -- Last line of inline assembly block 49 local BLOCK_ASM = 3 -- The whole block is an inline assembly block 50 51 -- Regex compilation cache 52 local regexp_compile_cache = {} 53 54 -- Error suppression state 55 local error_suppressions = {} 56 local error_suppressions_2 = {} 57 58 -- Configuration 59 local valid_extensions = { c = true, h = true } 60 61 -- Precompiled regex patterns (only the ones still used) 62 local RE_SUPPRESSION = vim.regex([[\<NOLINT\>]]) 63 local RE_COMMENTLINE = vim.regex([[^\s*//]]) 64 local RE_PATTERN_INCLUDE = vim.regex([[^\s*#\s*include\s*\([<"]\)\([^>"]*\)[>"].*$]]) 65 66 -- Assembly block matching (using Lua pattern instead of vim.regex for simplicity) 67 local function match_asm(line) 68 return line:find('^%s*asm%s*[{(]') 69 or line:find('^%s*_asm%s*[{(]') 70 or line:find('^%s*__asm%s*[{(]') 71 or line:find('^%s*__asm__%s*[{(]') 72 end 73 74 -- Threading function replacements 75 local threading_list = { 76 { 'asctime(', 'os_asctime_r(' }, 77 { 'ctime(', 'os_ctime_r(' }, 78 { 'getgrgid(', 'os_getgrgid_r(' }, 79 { 'getgrnam(', 'os_getgrnam_r(' }, 80 { 'getlogin(', 'os_getlogin_r(' }, 81 { 'getpwnam(', 'os_getpwnam_r(' }, 82 { 'getpwuid(', 'os_getpwuid_r(' }, 83 { 'gmtime(', 'os_gmtime_r(' }, 84 { 'localtime(', 'os_localtime_r(' }, 85 { 'strtok(', 'os_strtok_r(' }, 86 { 'ttyname(', 'os_ttyname_r(' }, 87 { 'asctime_r(', 'os_asctime_r(' }, 88 { 'ctime_r(', 'os_ctime_r(' }, 89 { 'getgrgid_r(', 'os_getgrgid_r(' }, 90 { 'getgrnam_r(', 'os_getgrnam_r(' }, 91 { 'getlogin_r(', 'os_getlogin_r(' }, 92 { 'getpwnam_r(', 'os_getpwnam_r(' }, 93 { 'getpwuid_r(', 'os_getpwuid_r(' }, 94 { 'gmtime_r(', 'os_gmtime_r(' }, 95 { 'localtime_r(', 'os_localtime_r(' }, 96 { 'strtok_r(', 'os_strtok_r(' }, 97 { 'ttyname_r(', 'os_ttyname_r(' }, 98 } 99 100 -- Memory function replacements 101 local memory_functions = { 102 { 'malloc(', 'xmalloc(' }, 103 { 'calloc(', 'xcalloc(' }, 104 { 'realloc(', 'xrealloc(' }, 105 { 'strdup(', 'xstrdup(' }, 106 { 'free(', 'xfree(' }, 107 } 108 local memory_ignore_pattern = vim.regex([[src/nvim/memory.c$]]) 109 110 -- OS function replacements 111 local os_functions = { 112 { 'setenv(', 'os_setenv(' }, 113 { 'getenv(', 'os_getenv(' }, 114 { '_wputenv(', 'os_setenv(' }, 115 { '_putenv_s(', 'os_setenv(' }, 116 { 'putenv(', 'os_setenv(' }, 117 { 'unsetenv(', 'os_unsetenv(' }, 118 } 119 120 -- CppLintState class equivalent 121 local CppLintState = {} 122 CppLintState.__index = CppLintState 123 124 function CppLintState.new() 125 local self = setmetatable({}, CppLintState) 126 self.verbose_level = 1 127 self.error_count = 0 128 self.filters = vim.deepcopy(DEFAULT_FILTERS) 129 self.counting = 'total' 130 self.errors_by_category = {} 131 self.stdin_filename = '' 132 self.output_format = 'emacs' 133 self.record_errors_file = nil 134 self.suppressed_errors = vim.defaulttable(function() 135 return vim.defaulttable(function() 136 return {} 137 end) 138 end) 139 return self 140 end 141 142 function CppLintState:set_output_format(output_format) 143 self.output_format = output_format 144 end 145 146 function CppLintState:set_verbose_level(level) 147 local last_verbose_level = self.verbose_level 148 self.verbose_level = level 149 return last_verbose_level 150 end 151 152 function CppLintState:set_counting_style(counting_style) 153 self.counting = counting_style 154 end 155 156 function CppLintState:set_filters(filters) 157 self.filters = vim.deepcopy(DEFAULT_FILTERS) 158 for filt in vim.gsplit(filters, ',', { trimempty = true }) do 159 local clean_filt = vim.trim(filt) 160 if clean_filt ~= '' then 161 table.insert(self.filters, clean_filt) 162 end 163 end 164 165 for _, filt in ipairs(self.filters) do 166 if not (filt:sub(1, 1) == '+' or filt:sub(1, 1) == '-') then 167 error('Every filter in --filters must start with + or - (' .. filt .. ' does not)') 168 end 169 end 170 end 171 172 function CppLintState:reset_error_counts() 173 self.error_count = 0 174 self.errors_by_category = {} 175 end 176 177 function CppLintState:increment_error_count(category) 178 self.error_count = self.error_count + 1 179 if self.counting == 'toplevel' or self.counting == 'detailed' then 180 local cat = category 181 if self.counting ~= 'detailed' then 182 cat = category:match('([^/]+)') or category 183 end 184 if not self.errors_by_category[cat] then 185 self.errors_by_category[cat] = 0 186 end 187 self.errors_by_category[cat] = self.errors_by_category[cat] + 1 188 end 189 end 190 191 function CppLintState:print_error_counts() 192 for category, count in pairs(self.errors_by_category) do 193 io.write(string.format("Category '%s' errors found: %d\n", category, count)) 194 end 195 if self.error_count > 0 then 196 io.write(string.format('Total errors found: %d\n', self.error_count)) 197 end 198 end 199 200 function CppLintState:suppress_errors_from(fname) 201 if not fname then 202 return 203 end 204 205 local ok, content = pcall(vim.fn.readfile, fname) 206 if not ok then 207 return 208 end 209 210 for _, line in ipairs(content) do 211 local ok2, data = pcall(vim.json.decode, line) 212 if ok2 then 213 local fname2, lines, category = data[1], data[2], data[3] 214 local lines_tuple = vim.tbl_islist(lines) and lines or { lines } 215 self.suppressed_errors[fname2][vim.inspect(lines_tuple)][category] = true 216 end 217 end 218 end 219 220 function CppLintState:record_errors_to(fname) 221 if not fname then 222 return 223 end 224 self.record_errors_file = io.open(fname, 'w') 225 end 226 227 -- Global state instance 228 local cpplint_state = CppLintState.new() 229 230 -- Utility functions 231 local function match(pattern, s) 232 if not regexp_compile_cache[pattern] then 233 regexp_compile_cache[pattern] = vim.regex(pattern) 234 end 235 local s_idx, e_idx = regexp_compile_cache[pattern]:match_str(s) 236 if s_idx then 237 local match_obj = {} 238 match_obj.start = s_idx 239 match_obj.finish = e_idx 240 function match_obj.group(n) 241 if n == 0 then 242 return s:sub(s_idx + 1, e_idx) 243 else 244 -- For subgroups, we need to use a different approach 245 -- This is a simplified version - full regex groups would need more complex handling 246 return s:sub(s_idx + 1, e_idx) 247 end 248 end 249 return match_obj 250 end 251 return nil 252 end 253 254 -- NOLINT suppression functions 255 local function parse_nolint_suppressions(raw_line, linenum) 256 local s_idx, e_idx = RE_SUPPRESSION:match_str(raw_line) 257 if not s_idx then 258 return 259 end 260 261 -- Extract what comes after NOLINT, looking for optional (category) 262 local after_nolint = raw_line:sub(e_idx + 1) 263 local category = after_nolint:match('^%s*(%([^)]*%))') 264 265 if not category or category == '(*)' then 266 -- Suppress all errors on this line 267 if not error_suppressions[vim.NIL] then 268 error_suppressions[vim.NIL] = {} 269 end 270 table.insert(error_suppressions[vim.NIL], linenum) 271 else 272 -- Extract category name from parentheses 273 local cat_name = category:match('^%((.-)%)$') 274 if cat_name then 275 for _, cat in ipairs(ERROR_CATEGORIES) do 276 if cat == cat_name then 277 if not error_suppressions[cat_name] then 278 error_suppressions[cat_name] = {} 279 end 280 table.insert(error_suppressions[cat_name], linenum) 281 break 282 end 283 end 284 end 285 end 286 end 287 288 local function reset_nolint_suppressions() 289 error_suppressions = {} 290 end 291 292 local function reset_known_error_suppressions() 293 error_suppressions_2 = {} 294 end 295 296 local function is_error_suppressed_by_nolint(category, linenum) 297 local cat_suppressed = error_suppressions[category] or {} 298 local all_suppressed = error_suppressions[vim.NIL] or {} 299 300 for _, line in ipairs(cat_suppressed) do 301 if line == linenum then 302 return true 303 end 304 end 305 for _, line in ipairs(all_suppressed) do 306 if line == linenum then 307 return true 308 end 309 end 310 return false 311 end 312 313 local function is_error_in_suppressed_errors_list(category, linenum) 314 local key = category .. ':' .. linenum 315 return error_suppressions_2[key] == true 316 end 317 318 -- FileInfo class equivalent 319 local FileInfo = {} 320 FileInfo.__index = FileInfo 321 322 function FileInfo.new(filename) 323 local self = setmetatable({}, FileInfo) 324 self._filename = filename 325 return self 326 end 327 328 function FileInfo:full_name() 329 local abspath = vim.fn.fnamemodify(self._filename, ':p') 330 return abspath:gsub('\\', '/') 331 end 332 333 function FileInfo:relative_path() 334 local fullname = self:full_name() 335 336 if vim.fn.filereadable(fullname) == 1 then 337 -- Find git repository root using vim.fs.root 338 local git_root = vim.fs.root(fullname, '.git') 339 if git_root then 340 local root_dir = vim.fs.joinpath(git_root, 'src', 'nvim') 341 local relpath = vim.fs.relpath(root_dir, fullname) 342 if relpath then 343 return relpath 344 end 345 end 346 end 347 348 return fullname 349 end 350 351 -- Error reporting 352 local function should_print_error(category, confidence, linenum) 353 if is_error_suppressed_by_nolint(category, linenum) then 354 return false 355 end 356 if is_error_in_suppressed_errors_list(category, linenum) then 357 return false 358 end 359 if confidence < cpplint_state.verbose_level then 360 return false 361 end 362 363 local is_filtered = false 364 for _, one_filter in ipairs(cpplint_state.filters) do 365 if one_filter:sub(1, 1) == '-' then 366 if category:find(one_filter:sub(2), 1, true) == 1 then 367 is_filtered = true 368 end 369 elseif one_filter:sub(1, 1) == '+' then 370 if category:find(one_filter:sub(2), 1, true) == 1 then 371 is_filtered = false 372 end 373 end 374 end 375 376 return not is_filtered 377 end 378 379 local function error_func(filename, linenum, category, confidence, message) 380 if should_print_error(category, confidence, linenum) then 381 cpplint_state:increment_error_count(category) 382 383 if cpplint_state.output_format == 'vs7' then 384 io.write( 385 string.format('%s(%s): %s [%s] [%d]\n', filename, linenum, message, category, confidence) 386 ) 387 elseif cpplint_state.output_format == 'eclipse' then 388 io.write( 389 string.format( 390 '%s:%s: warning: %s [%s] [%d]\n', 391 filename, 392 linenum, 393 message, 394 category, 395 confidence 396 ) 397 ) 398 elseif cpplint_state.output_format == 'gh_action' then 399 io.write( 400 string.format( 401 '::error file=%s,line=%s::%s [%s] [%d]\n', 402 filename, 403 linenum, 404 message, 405 category, 406 confidence 407 ) 408 ) 409 else 410 io.write( 411 string.format('%s:%s: %s [%s] [%d]\n', filename, linenum, message, category, confidence) 412 ) 413 end 414 end 415 end 416 417 -- String processing functions 418 local function is_cpp_string(line) 419 line = line:gsub('\\\\', 'XX') 420 local quote_count = select(2, line:gsub('"', '')) 421 local escaped_quote_count = select(2, line:gsub('\\"', '')) 422 local combined_quote_count = select(2, line:gsub("'\"'", '')) 423 return ((quote_count - escaped_quote_count - combined_quote_count) % 2) == 1 424 end 425 426 local function cleanse_comments(line) 427 local commentpos = line:find('//') 428 if commentpos and not is_cpp_string(line:sub(1, commentpos - 1)) then 429 line = line:sub(1, commentpos - 1):gsub('%s+$', '') 430 end 431 -- Remove /* */ comments 432 line = line:gsub('/%*.-%*/', '') 433 return line 434 end 435 436 -- CleansedLines class equivalent 437 local CleansedLines = {} 438 CleansedLines.__index = CleansedLines 439 440 function CleansedLines.new(lines, init_lines) 441 local self = setmetatable({}, CleansedLines) 442 self.elided = {} 443 self.lines = {} 444 self.raw_lines = lines 445 self._num_lines = #lines 446 self.init_lines = init_lines 447 self.lines_without_raw_strings = lines 448 self.elided_with_space_strings = {} 449 450 for linenum = 1, #self.lines_without_raw_strings do 451 local line = self.lines_without_raw_strings[linenum] 452 table.insert(self.lines, cleanse_comments(line)) 453 454 local elided = self:_collapse_strings(line) 455 table.insert(self.elided, cleanse_comments(elided)) 456 457 local elided_spaces = self:_collapse_strings(line, true) 458 table.insert(self.elided_with_space_strings, cleanse_comments(elided_spaces)) 459 end 460 461 return self 462 end 463 464 function CleansedLines:num_lines() 465 return self._num_lines 466 end 467 468 function CleansedLines:_collapse_strings(elided, keep_spaces) 469 if not RE_PATTERN_INCLUDE:match_str(elided) then 470 -- Remove escaped characters 471 elided = elided:gsub('\\[abfnrtv?"\\\'\\]', keep_spaces and ' ' or '') 472 elided = elided:gsub('\\%d+', keep_spaces and ' ' or '') 473 elided = elided:gsub('\\x[0-9a-fA-F]+', keep_spaces and ' ' or '') 474 475 if keep_spaces then 476 elided = elided:gsub("'([^'])'", function(c) 477 return "'" .. string.rep(' ', #c) .. "'" 478 end) 479 elided = elided:gsub('"([^"]*)"', function(c) 480 return '"' .. string.rep(' ', #c) .. '"' 481 end) 482 else 483 elided = elided:gsub("'([^'])'", "''") 484 elided = elided:gsub('"([^"]*)"', '""') 485 end 486 end 487 return elided 488 end 489 490 -- Helper functions for argument parsing 491 local function print_usage(message) 492 local usage = [[ 493 Syntax: clint.lua [--verbose=#] [--output=vs7] [--filter=-x,+y,...] 494 [--counting=total|toplevel|detailed] [--root=subdir] 495 [--linelength=digits] [--record-errors=file] 496 [--suppress-errors=file] [--stdin-filename=filename] 497 <file> [file] ... 498 499 The style guidelines this tries to follow are those in 500 https://neovim.io/doc/user/dev_style.html#dev-style 501 502 Note: This is Google's https://github.com/cpplint/cpplint modified for use 503 with the Neovim project. 504 505 Every problem is given a confidence score from 1-5, with 5 meaning we are 506 certain of the problem, and 1 meaning it could be a legitimate construct. 507 This will miss some errors, and is not a substitute for a code review. 508 509 To suppress false-positive errors of a certain category, add a 510 'NOLINT(category)' comment to the line. NOLINT or NOLINT(*) 511 suppresses errors of all categories on that line. 512 513 The files passed in will be linted; at least one file must be provided. 514 Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the 515 extensions with the --extensions flag. 516 517 Flags: 518 519 output=vs7 520 By default, the output is formatted to ease emacs parsing. Visual Studio 521 compatible output (vs7) may also be used. Other formats are unsupported. 522 523 verbose=# 524 Specify a number 0-5 to restrict errors to certain verbosity levels. 525 526 filter=-x,+y,... 527 Specify a comma-separated list of category-filters to apply: only 528 error messages whose category names pass the filters will be printed. 529 (Category names are printed with the message and look like 530 "[whitespace/indent]".) Filters are evaluated left to right. 531 "-FOO" and "FOO" means "do not print categories that start with FOO". 532 "+FOO" means "do print categories that start with FOO". 533 534 Examples: --filter=-whitespace,+whitespace/braces 535 --filter=whitespace,runtime/printf,+runtime/printf_format 536 --filter=-,+build/include_what_you_use 537 538 To see a list of all the categories used in cpplint, pass no arg: 539 --filter= 540 541 counting=total|toplevel|detailed 542 The total number of errors found is always printed. If 543 'toplevel' is provided, then the count of errors in each of 544 the top-level categories like 'build' and 'whitespace' will 545 also be printed. If 'detailed' is provided, then a count 546 is provided for each category. 547 548 root=subdir 549 The root directory used for deriving header guard CPP variable. 550 By default, the header guard CPP variable is calculated as the relative 551 path to the directory that contains .git, .hg, or .svn. When this flag 552 is specified, the relative path is calculated from the specified 553 directory. If the specified directory does not exist, this flag is 554 ignored. 555 556 Examples: 557 Assuing that src/.git exists, the header guard CPP variables for 558 src/chrome/browser/ui/browser.h are: 559 560 No flag => CHROME_BROWSER_UI_BROWSER_H_ 561 --root=chrome => BROWSER_UI_BROWSER_H_ 562 --root=chrome/browser => UI_BROWSER_H_ 563 564 linelength=digits 565 This is the allowed line length for the project. The default value is 566 80 characters. 567 568 Examples: 569 --linelength=120 570 571 extensions=extension,extension,... 572 The allowed file extensions that cpplint will check 573 574 Examples: 575 --extensions=hpp,cpp 576 577 record-errors=file 578 Record errors to the given location. This file may later be used for error 579 suppression using suppress-errors flag. 580 581 suppress-errors=file 582 Errors listed in the given file will not be reported. 583 584 stdin-filename=filename 585 Use specified filename when reading from stdin (file "-"). 586 ]] 587 588 if message then 589 io.stderr:write(usage .. '\nFATAL ERROR: ' .. message .. '\n') 590 os.exit(1) 591 else 592 io.write(usage) 593 os.exit(0) 594 end 595 end 596 597 local function print_categories() 598 for _, cat in ipairs(ERROR_CATEGORIES) do 599 io.write(' ' .. cat .. '\n') 600 end 601 os.exit(0) 602 end 603 604 -- Argument parsing 605 local function parse_arguments(args) 606 local filenames = {} 607 local opts = { 608 output_format = 'emacs', 609 verbose_level = 1, 610 filters = '', 611 counting_style = 'total', 612 extensions = { 'c', 'h' }, 613 record_errors_file = nil, 614 suppress_errors_file = nil, 615 stdin_filename = '', 616 } 617 618 local i = 1 619 while i <= #args do 620 local arg = args[i] 621 622 if arg == '--help' then 623 print_usage() 624 elseif arg:sub(1, 9) == '--output=' then 625 local format = arg:sub(10) 626 if 627 format ~= 'emacs' 628 and format ~= 'vs7' 629 and format ~= 'eclipse' 630 and format ~= 'gh_action' 631 then 632 print_usage('The only allowed output formats are emacs, vs7 and eclipse.') 633 end 634 opts.output_format = format 635 elseif arg:sub(1, 10) == '--verbose=' then 636 opts.verbose_level = tonumber(arg:sub(11)) 637 elseif arg:sub(1, 9) == '--filter=' then 638 opts.filters = arg:sub(10) 639 if opts.filters == '' then 640 print_categories() 641 end 642 elseif arg:sub(1, 12) == '--counting=' then 643 local style = arg:sub(13) 644 if style ~= 'total' and style ~= 'toplevel' and style ~= 'detailed' then 645 print_usage('Valid counting options are total, toplevel, and detailed') 646 end 647 opts.counting_style = style 648 elseif arg:sub(1, 13) == '--extensions=' then 649 local exts = arg:sub(14) 650 opts.extensions = {} 651 for ext in vim.gsplit(exts, ',', { trimempty = true }) do 652 table.insert(opts.extensions, vim.trim(ext)) 653 end 654 elseif arg:sub(1, 16) == '--record-errors=' then 655 opts.record_errors_file = arg:sub(17) 656 elseif arg:sub(1, 18) == '--suppress-errors=' then 657 opts.suppress_errors_file = arg:sub(19) 658 elseif arg:sub(1, 17) == '--stdin-filename=' then 659 opts.stdin_filename = arg:sub(18) 660 elseif arg:sub(1, 2) == '--' then 661 print_usage('Unknown option: ' .. arg) 662 else 663 table.insert(filenames, arg) 664 end 665 666 i = i + 1 667 end 668 669 if #filenames == 0 then 670 print_usage('No files were specified.') 671 end 672 673 return filenames, opts 674 end 675 676 -- Lint checking functions 677 678 local function find_next_multiline_comment_start(lines, lineix) 679 while lineix <= #lines do 680 if lines[lineix]:find('^%s*/%*') then 681 if not lines[lineix]:find('%*/', 1, true) then 682 -- Check if this line ends with backslash (line continuation) 683 -- If so, don't treat it as a multiline comment start 684 local line = lines[lineix] 685 if not line:find('\\%s*$') then 686 return lineix 687 end 688 end 689 end 690 lineix = lineix + 1 691 end 692 return #lines + 1 693 end 694 695 local function find_next_multiline_comment_end(lines, lineix) 696 while lineix <= #lines do 697 if lines[lineix]:find('%*/%s*$') then 698 return lineix 699 end 700 lineix = lineix + 1 701 end 702 return #lines + 1 703 end 704 705 local function remove_multiline_comments_from_range(lines, begin, finish) 706 for i = begin, finish do 707 lines[i] = '// dummy' 708 end 709 end 710 711 local function remove_multiline_comments(filename, lines, error) 712 local lineix = 1 713 while lineix <= #lines do 714 local lineix_begin = find_next_multiline_comment_start(lines, lineix) 715 if lineix_begin > #lines then 716 return 717 end 718 local lineix_end = find_next_multiline_comment_end(lines, lineix_begin) 719 if lineix_end > #lines then 720 error( 721 filename, 722 lineix_begin, 723 'readability/multiline_comment', 724 5, 725 'Could not find end of multi-line comment' 726 ) 727 return 728 end 729 remove_multiline_comments_from_range(lines, lineix_begin, lineix_end) 730 lineix = lineix_end + 1 731 end 732 end 733 734 local function check_for_header_guard(filename, lines, error) 735 if filename:match('%.c%.h$') or FileInfo.new(filename):relative_path() == 'func_attr.h' then 736 return 737 end 738 739 local found_pragma = false 740 for _, line in ipairs(lines) do 741 if line:find('#pragma%s+once') then 742 found_pragma = true 743 break 744 end 745 end 746 747 if not found_pragma then 748 error(filename, 1, 'build/header_guard', 5, 'No "#pragma once" found in header') 749 end 750 end 751 752 local function check_includes(filename, lines, error) 753 if 754 filename:match('%.c%.h$') 755 or filename:match('%.in%.h$') 756 or FileInfo.new(filename):relative_path() == 'func_attr.h' 757 or FileInfo.new(filename):relative_path() == 'os/pty_proc.h' 758 then 759 return 760 end 761 762 local check_includes_ignore = { 763 'src/nvim/api/private/validate.h', 764 'src/nvim/assert_defs.h', 765 'src/nvim/channel.h', 766 'src/nvim/charset.h', 767 'src/nvim/eval/typval.h', 768 'src/nvim/event/multiqueue.h', 769 'src/nvim/garray.h', 770 'src/nvim/globals.h', 771 'src/nvim/highlight.h', 772 'src/nvim/lua/executor.h', 773 'src/nvim/main.h', 774 'src/nvim/mark.h', 775 'src/nvim/msgpack_rpc/channel_defs.h', 776 'src/nvim/msgpack_rpc/unpacker.h', 777 'src/nvim/option.h', 778 'src/nvim/os/pty_conpty_win.h', 779 'src/nvim/os/pty_proc_win.h', 780 } 781 782 local skip_headers = { 783 'auto/config.h', 784 'klib/klist.h', 785 'klib/kvec.h', 786 'mpack/mpack_core.h', 787 'mpack/object.h', 788 'nvim/func_attr.h', 789 'termkey/termkey.h', 790 'vterm/vterm.h', 791 'xdiff/xdiff.h', 792 } 793 794 for _, ignore in ipairs(check_includes_ignore) do 795 if filename:match(ignore .. '$') then 796 return 797 end 798 end 799 800 for i, line in ipairs(lines) do 801 local matched = match('#\\s*include\\s*"([^"]*)"', line) 802 if matched then 803 local name = line:match('#\\s*include\\s*"([^"]*)"') 804 local should_skip = false 805 for _, skip in ipairs(skip_headers) do 806 if name == skip then 807 should_skip = true 808 break 809 end 810 end 811 812 if 813 not should_skip 814 and not name:match('%.h%.generated%.h$') 815 and not name:match('/defs%.h$') 816 and not name:match('_defs%.h$') 817 and not name:match('%.h%.inline%.generated%.h$') 818 and not name:match('_defs%.generated%.h$') 819 and not name:match('_enum%.generated%.h$') 820 then 821 error( 822 filename, 823 i - 1, 824 'build/include_defs', 825 5, 826 'Headers should not include non-"_defs" headers' 827 ) 828 end 829 end 830 end 831 end 832 833 local function check_non_symbols(filename, lines, error) 834 for i, line in ipairs(lines) do 835 if line:match('^EXTERN ') or line:match('^extern ') then 836 error( 837 filename, 838 i - 1, 839 'build/defs_header', 840 5, 841 '"_defs" headers should not contain extern variables' 842 ) 843 end 844 end 845 end 846 847 local function check_for_bad_characters(filename, lines, error) 848 for linenum, line in ipairs(lines) do 849 if line:find('\239\187\191') then -- UTF-8 BOM 850 error( 851 filename, 852 linenum - 1, 853 'readability/utf8', 854 5, 855 'Line contains invalid UTF-8 (or Unicode replacement character).' 856 ) 857 end 858 if line:find('\0') then 859 error(filename, linenum - 1, 'readability/nul', 5, 'Line contains NUL byte.') 860 end 861 end 862 end 863 864 local function check_for_multiline_comments_and_strings(filename, clean_lines, linenum, error) 865 -- Use elided line (with strings collapsed) to avoid false positives from /* */ in strings 866 local line = clean_lines.elided[linenum + 1] 867 if not line then 868 return 869 end 870 871 -- Remove all \\ (escaped backslashes) from the line. They are OK, and the 872 -- second (escaped) slash may trigger later \" detection erroneously. 873 line = line:gsub('\\\\', '') 874 875 local comment_count = select(2, line:gsub('/%*', '')) 876 local comment_end_count = select(2, line:gsub('%*/', '')) 877 -- Only warn if there are actually more opening than closing comments 878 -- (accounting for the possibility that this is a multi-line comment that continues) 879 if comment_count > comment_end_count and comment_count > 0 then 880 error( 881 filename, 882 linenum, 883 'readability/multiline_comment', 884 5, 885 'Complex multi-line /*...*/-style comment found. ' 886 .. 'Lint may give bogus warnings. ' 887 .. 'Consider replacing these with //-style comments, ' 888 .. 'with #if 0...#endif, ' 889 .. 'or with more clearly structured multi-line comments.' 890 ) 891 end 892 893 -- Dropped 'readability/multiline_string' detection because it produces too many false positives 894 -- with escaped quotes in C strings and character literals. 895 end 896 897 local function check_for_old_style_comments(filename, line, linenum, error) 898 if line:find('/%*') and line:sub(-1) ~= '\\' and not RE_COMMENTLINE:match_str(line) then 899 error( 900 filename, 901 linenum, 902 'readability/old_style_comment', 903 5, 904 '/*-style comment found, it should be replaced with //-style. ' 905 .. '/*-style comments are only allowed inside macros. ' 906 .. 'Note that you should not use /*-style comments to document ' 907 .. 'macros itself, use doxygen-style comments for this.' 908 ) 909 end 910 end 911 912 local function check_posix_threading(filename, clean_lines, linenum, error) 913 local line = clean_lines.elided[linenum + 1] 914 915 for _, pair in ipairs(threading_list) do 916 local single_thread_function, multithread_safe_function = pair[1], pair[2] 917 local start_pos = line:find(single_thread_function, 1, true) 918 919 if start_pos then 920 local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' 921 if 922 start_pos == 1 923 or ( 924 not prev_char:match('%w') 925 and prev_char ~= '_' 926 and prev_char ~= '.' 927 and prev_char ~= '>' 928 ) 929 then 930 error( 931 filename, 932 linenum, 933 'runtime/threadsafe_fn', 934 2, 935 'Use ' 936 .. multithread_safe_function 937 .. '...) instead of ' 938 .. single_thread_function 939 .. '...). If it is missing, consider implementing it;' 940 .. ' see os_localtime_r for an example.' 941 ) 942 end 943 end 944 end 945 end 946 947 local function check_memory_functions(filename, clean_lines, linenum, error) 948 if memory_ignore_pattern:match_str(filename) then 949 return 950 end 951 952 local line = clean_lines.elided[linenum + 1] 953 954 for _, pair in ipairs(memory_functions) do 955 local func, suggested_func = pair[1], pair[2] 956 local start_pos = line:find(func, 1, true) 957 958 if start_pos then 959 local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' 960 if 961 start_pos == 1 962 or ( 963 not prev_char:match('%w') 964 and prev_char ~= '_' 965 and prev_char ~= '.' 966 and prev_char ~= '>' 967 ) 968 then 969 error( 970 filename, 971 linenum, 972 'runtime/memory_fn', 973 2, 974 'Use ' .. suggested_func .. '...) instead of ' .. func .. '...).' 975 ) 976 end 977 end 978 end 979 end 980 981 local function check_os_functions(filename, clean_lines, linenum, error) 982 local line = clean_lines.elided[linenum + 1] 983 984 for _, pair in ipairs(os_functions) do 985 local func, suggested_func = pair[1], pair[2] 986 local start_pos = line:find(func, 1, true) 987 988 if start_pos then 989 local prev_char = start_pos > 1 and line:sub(start_pos - 1, start_pos - 1) or '' 990 if 991 start_pos == 1 992 or ( 993 not prev_char:match('%w') 994 and prev_char ~= '_' 995 and prev_char ~= '.' 996 and prev_char ~= '>' 997 ) 998 then 999 error( 1000 filename, 1001 linenum, 1002 'runtime/os_fn', 1003 2, 1004 'Use ' .. suggested_func .. '...) instead of ' .. func .. '...).' 1005 ) 1006 end 1007 end 1008 end 1009 end 1010 1011 local function check_language(filename, clean_lines, linenum, error) 1012 local line = clean_lines.elided[linenum + 1] 1013 if not line or line == '' then 1014 return 1015 end 1016 1017 -- Check for verboten C basic types 1018 local short_regex = vim.regex([[\<short\>]]) 1019 local long_long_regex = vim.regex([[\<long\> \+\<long\>]]) 1020 1021 if short_regex:match_str(line) then 1022 error( 1023 filename, 1024 linenum, 1025 'runtime/int', 1026 4, 1027 'Use int16_t/int64_t/etc, rather than the C type short' 1028 ) 1029 elseif long_long_regex:match_str(line) then 1030 error( 1031 filename, 1032 linenum, 1033 'runtime/int', 1034 4, 1035 'Use int16_t/int64_t/etc, rather than the C type long long' 1036 ) 1037 end 1038 1039 -- Check for snprintf with non-zero literal size 1040 local snprintf_match = line:match('snprintf%s*%([^,]*,%s*([0-9]+)%s*,') 1041 if snprintf_match and snprintf_match ~= '0' then 1042 error( 1043 filename, 1044 linenum, 1045 'runtime/printf', 1046 3, 1047 'If you can, use sizeof(...) instead of ' .. snprintf_match .. ' as the 2nd arg to snprintf.' 1048 ) 1049 end 1050 1051 -- Check for sprintf (use vim.regex for proper word boundaries) 1052 local sprintf_regex = vim.regex([[\<sprintf\>]]) 1053 if sprintf_regex:match_str(line) then 1054 error(filename, linenum, 'runtime/printf', 5, 'Use snprintf instead of sprintf.') 1055 end 1056 1057 -- Check for strncpy (use vim.regex for proper word boundaries) 1058 local strncpy_regex = vim.regex([[\<strncpy\>]]) 1059 local strncpy_upper_regex = vim.regex([[\<STRNCPY\>]]) 1060 if strncpy_regex:match_str(line) then 1061 error( 1062 filename, 1063 linenum, 1064 'runtime/printf', 1065 4, 1066 'Use xstrlcpy, xmemcpyz or snprintf instead of strncpy (unless this is from Vim)' 1067 ) 1068 elseif strncpy_upper_regex:match_str(line) then 1069 error( 1070 filename, 1071 linenum, 1072 'runtime/printf', 1073 4, 1074 'Use xstrlcpy, xmemcpyz or snprintf instead of STRNCPY (unless this is from Vim)' 1075 ) 1076 end 1077 1078 -- Check for strcpy (use vim.regex for proper word boundaries) 1079 local strcpy_regex = vim.regex([[\<strcpy\>]]) 1080 if strcpy_regex:match_str(line) then 1081 error( 1082 filename, 1083 linenum, 1084 'runtime/printf', 1085 4, 1086 'Use xstrlcpy, xmemcpyz or snprintf instead of strcpy' 1087 ) 1088 end 1089 1090 -- Check for memset with wrong argument order: memset(buf, sizeof(buf), 0) 1091 -- Pattern: memset(arg1, arg2, 0) where arg2 is NOT a valid fill value 1092 local memset_start = line:find('memset%s*%([^)]*,%s*[^,]*,%s*0%s*%)') 1093 if memset_start then 1094 -- Extract the full memset call 1095 local memset_part = line:sub(memset_start) 1096 local first_comma = memset_part:find(',') 1097 if first_comma then 1098 local after_first = memset_part:sub(first_comma + 1) 1099 local second_comma = after_first:find(',') 1100 if second_comma then 1101 local second_arg = vim.trim(after_first:sub(1, second_comma - 1)) 1102 local first_arg = vim.trim(memset_part:match('memset%s*%(%s*([^,]*)%s*,')) 1103 1104 -- Check if second_arg is NOT a simple literal value 1105 if 1106 second_arg ~= '' 1107 and second_arg ~= "''" 1108 and not second_arg:match('^%-?%d+$') 1109 and not second_arg:match('^0x[0-9a-fA-F]+$') 1110 then 1111 error( 1112 filename, 1113 linenum, 1114 'runtime/memset', 1115 4, 1116 'Did you mean "memset(' .. first_arg .. ', 0, ' .. second_arg .. ')"?' 1117 ) 1118 end 1119 end 1120 end 1121 end 1122 1123 -- Detect variable-length arrays 1124 -- Pattern: type varname[size]; where type is an identifier and varname starts with lowercase 1125 local var_type = line:match('%s*(%w+)%s+') 1126 if var_type and var_type ~= 'return' and var_type ~= 'delete' then 1127 -- Look for array declaration pattern 1128 local array_size = line:match('%w+%s+[a-z]%w*%s*%[([^%]]+)%]') 1129 if array_size and not array_size:find('%]') then -- Ensure no nested brackets (multidimensional arrays) 1130 -- Check if size is a compile-time constant 1131 local is_const = true 1132 1133 -- Split on common operators (space, +, -, *, /, <<, >>) 1134 local tokens = vim.split(array_size, '[%s%+%-%*%/%>%<]+') 1135 1136 for _, tok in ipairs(tokens) do 1137 tok = vim.trim(tok) 1138 if tok ~= '' then 1139 -- Check for sizeof(...) and arraysize(...) or ARRAY_SIZE(...) patterns (before stripping parens) 1140 local is_valid = tok:find('sizeof%(.+%)') 1141 or tok:find('arraysize%(%w+%)') 1142 or tok:find('ARRAY_SIZE%(.+%)') 1143 1144 if not is_valid then 1145 -- Strip leading and trailing parentheses for other checks 1146 tok = tok:gsub('^%(*', ''):gsub('%)*$', '') 1147 tok = vim.trim(tok) 1148 1149 if tok ~= '' then 1150 -- Allow: numeric literals, hex, k-prefixed constants, SCREAMING_CASE, sizeof, arraysize 1151 is_valid = ( 1152 tok:match('^%d+$') -- decimal number 1153 or tok:match('^0x[0-9a-fA-F]+$') -- hex number 1154 or tok:match('^k[A-Z0-9]') -- k-prefixed constant 1155 or tok:match('^[A-Z][A-Z0-9_]*$') -- SCREAMING_CASE 1156 or tok:match('^sizeof') -- sizeof(...) 1157 or tok:match('^arraysize') 1158 ) -- arraysize(...) 1159 end 1160 end 1161 1162 if not is_valid then 1163 is_const = false 1164 break 1165 end 1166 end 1167 end 1168 1169 if not is_const then 1170 error( 1171 filename, 1172 linenum, 1173 'runtime/arrays', 1174 1, 1175 "Do not use variable-length arrays. Use an appropriately named ('k' followed by CamelCase) compile-time constant for the size." 1176 ) 1177 end 1178 end 1179 end 1180 1181 -- Check for TRUE/FALSE (use vim.regex for proper word boundaries) 1182 local true_regex = vim.regex([[\<TRUE\>]]) 1183 local false_regex = vim.regex([[\<FALSE\>]]) 1184 local maybe_regex = vim.regex([[\<MAYBE\>]]) 1185 1186 if true_regex:match_str(line) then 1187 error(filename, linenum, 'readability/bool', 4, 'Use true instead of TRUE.') 1188 end 1189 1190 if false_regex:match_str(line) then 1191 error(filename, linenum, 'readability/bool', 4, 'Use false instead of FALSE.') 1192 end 1193 1194 -- Check for MAYBE 1195 if maybe_regex:match_str(line) then 1196 error(filename, linenum, 'readability/bool', 4, 'Use kNone from TriState instead of MAYBE.') 1197 end 1198 1199 -- Detect preincrement/predecrement at start of line 1200 if line:match('^%s*%+%+') or line:match('^%s*%-%-') then 1201 error( 1202 filename, 1203 linenum, 1204 'readability/increment', 1205 5, 1206 'Do not use preincrement in statements, use postincrement instead' 1207 ) 1208 end 1209 1210 -- Detect preincrement/predecrement in for(;; preincrement) 1211 -- Look for pattern like "; ++var" or "; --var" 1212 local last_semi_pos = 0 1213 for i = 1, #line do 1214 if line:sub(i, i) == ';' then 1215 last_semi_pos = i 1216 end 1217 end 1218 1219 if last_semi_pos > 0 then 1220 -- Check if there's a preincrement/predecrement after the last semicolon 1221 local after_semi = line:sub(last_semi_pos + 1) 1222 local op_pos = after_semi:find('%+%+') 1223 if not op_pos then 1224 op_pos = after_semi:find('%-%-') 1225 end 1226 if op_pos then 1227 -- Found preincrement/predecrement after last semicolon 1228 local expr_start = after_semi:sub(1, op_pos - 1):match('^%s*(.*)') 1229 if not expr_start or expr_start == '' then 1230 -- Nothing but whitespace before operator, check the expression 1231 local expr_text = after_semi:sub(op_pos) 1232 if not expr_text:find(';') and not expr_text:find(' = ') then 1233 error( 1234 filename, 1235 linenum, 1236 'readability/increment', 1237 4, 1238 'Do not use preincrement in statements, including for(;; action)' 1239 ) 1240 end 1241 end 1242 end 1243 end 1244 end 1245 1246 local function check_for_non_standard_constructs(filename, clean_lines, linenum, error) 1247 local line = clean_lines.lines[linenum + 1] 1248 1249 -- Check for printf format issues with %q and %N$ in quoted strings 1250 -- Extract all quoted strings and check their format specifiers 1251 for str in line:gmatch('"([^"]*)"') do 1252 -- Check for %q format (deprecated) 1253 if str:find('%%%-?%+?%s?%d*q') then 1254 error( 1255 filename, 1256 linenum, 1257 'runtime/printf_format', 1258 3, 1259 '"%q" in format strings is deprecated. Use "%" PRId64 instead.' 1260 ) 1261 end 1262 1263 -- Check for %N$ format (unconventional positional specifier) 1264 if str:find('%%%d+%$') then 1265 error( 1266 filename, 1267 linenum, 1268 'runtime/printf_format', 1269 2, 1270 '%N$ formats are unconventional. Try rewriting to avoid them.' 1271 ) 1272 end 1273 end 1274 1275 -- Check for storage class order (type before storage class modifier) 1276 -- Match type keywords followed by storage class keywords 1277 local type_keywords = { 1278 'const', 1279 'volatile', 1280 'void', 1281 'char', 1282 'short', 1283 'int', 1284 'long', 1285 'float', 1286 'double', 1287 'signed', 1288 'unsigned', 1289 } 1290 local storage_keywords = { 'register', 'static', 'extern', 'typedef' } 1291 1292 for _, type_kw in ipairs(type_keywords) do 1293 for _, storage_kw in ipairs(storage_keywords) do 1294 local pattern = '\\<' .. type_kw .. '\\>\\s\\+\\<' .. storage_kw .. '\\>' 1295 if vim.regex(pattern):match_str(line) then 1296 error( 1297 filename, 1298 linenum, 1299 'build/storage_class', 1300 5, 1301 'Storage class (static, extern, typedef, etc) should be first.' 1302 ) 1303 return 1304 end 1305 end 1306 end 1307 1308 -- Check for endif comments 1309 if line:match('^%s*#%s*endif%s*[^/\\s]+') then 1310 error( 1311 filename, 1312 linenum, 1313 'build/endif_comment', 1314 5, 1315 'Uncommented text after #endif is non-standard. Use a comment.' 1316 ) 1317 end 1318 end 1319 1320 -- Nesting state classes 1321 local BlockInfo = {} 1322 BlockInfo.__index = BlockInfo 1323 1324 function BlockInfo.new(seen_open_brace) 1325 local self = setmetatable({}, BlockInfo) 1326 self.seen_open_brace = seen_open_brace 1327 self.open_parentheses = 0 1328 self.inline_asm = NO_ASM 1329 return self 1330 end 1331 1332 local PreprocessorInfo = {} 1333 PreprocessorInfo.__index = PreprocessorInfo 1334 1335 function PreprocessorInfo.new(stack_before_if) 1336 local self = setmetatable({}, PreprocessorInfo) 1337 self.stack_before_if = stack_before_if 1338 self.stack_before_else = {} 1339 self.seen_else = false 1340 return self 1341 end 1342 1343 local NestingState = {} 1344 NestingState.__index = NestingState 1345 1346 function NestingState.new() 1347 local self = setmetatable({}, NestingState) 1348 self.stack = {} 1349 self.pp_stack = {} 1350 return self 1351 end 1352 1353 function NestingState:seen_open_brace() 1354 return #self.stack == 0 or self.stack[#self.stack].seen_open_brace 1355 end 1356 1357 function NestingState:update_preprocessor(line) 1358 if line:match('^%s*#%s*(if|ifdef|ifndef)') then 1359 table.insert(self.pp_stack, PreprocessorInfo.new(vim.deepcopy(self.stack))) 1360 elseif line:match('^%s*#%s*(else|elif)') then 1361 if #self.pp_stack > 0 then 1362 if not self.pp_stack[#self.pp_stack].seen_else then 1363 self.pp_stack[#self.pp_stack].seen_else = true 1364 self.pp_stack[#self.pp_stack].stack_before_else = vim.deepcopy(self.stack) 1365 end 1366 self.stack = vim.deepcopy(self.pp_stack[#self.pp_stack].stack_before_if) 1367 end 1368 elseif line:match('^%s*#%s*endif') then 1369 if #self.pp_stack > 0 then 1370 if self.pp_stack[#self.pp_stack].seen_else then 1371 self.stack = self.pp_stack[#self.pp_stack].stack_before_else 1372 end 1373 table.remove(self.pp_stack) 1374 end 1375 end 1376 end 1377 1378 function NestingState:update(clean_lines, linenum) 1379 local line = clean_lines.elided[linenum + 1] 1380 1381 self:update_preprocessor(line) 1382 1383 if #self.stack > 0 then 1384 local inner_block = self.stack[#self.stack] 1385 local depth_change = select(2, line:gsub('%(', '')) - select(2, line:gsub('%)', '')) 1386 inner_block.open_parentheses = inner_block.open_parentheses + depth_change 1387 1388 if inner_block.inline_asm == NO_ASM or inner_block.inline_asm == END_ASM then 1389 if depth_change ~= 0 and inner_block.open_parentheses == 1 and match_asm(line) then 1390 inner_block.inline_asm = INSIDE_ASM 1391 else 1392 inner_block.inline_asm = NO_ASM 1393 end 1394 elseif inner_block.inline_asm == INSIDE_ASM and inner_block.open_parentheses == 0 then 1395 inner_block.inline_asm = END_ASM 1396 end 1397 end 1398 1399 while true do 1400 local matched = line:match('^[^{;)}]*([{;)}])(.*)$') 1401 if not matched then 1402 break 1403 end 1404 1405 local token = matched:sub(1, 1) 1406 if token == '{' then 1407 if not self:seen_open_brace() then 1408 self.stack[#self.stack].seen_open_brace = true 1409 else 1410 table.insert(self.stack, BlockInfo.new(true)) 1411 if match_asm(line) then 1412 self.stack[#self.stack].inline_asm = BLOCK_ASM 1413 end 1414 end 1415 elseif token == ';' or token == ')' then 1416 if not self:seen_open_brace() then 1417 table.remove(self.stack) 1418 end 1419 else -- token == '}' 1420 if #self.stack > 0 then 1421 table.remove(self.stack) 1422 end 1423 end 1424 line = matched:sub(2) 1425 end 1426 end 1427 1428 -- Main processing functions 1429 local function process_line( 1430 filename, 1431 clean_lines, 1432 line, 1433 nesting_state, 1434 error, 1435 extra_check_functions 1436 ) 1437 local raw_lines = clean_lines.raw_lines 1438 local init_lines = clean_lines.init_lines 1439 1440 parse_nolint_suppressions(raw_lines[line + 1], line) 1441 nesting_state:update(clean_lines, line) 1442 1443 if 1444 #nesting_state.stack > 0 and nesting_state.stack[#nesting_state.stack].inline_asm ~= NO_ASM 1445 then 1446 return 1447 end 1448 1449 check_for_multiline_comments_and_strings(filename, clean_lines, line, error) 1450 check_for_old_style_comments(filename, init_lines[line + 1], line, error) 1451 check_language(filename, clean_lines, line, error) 1452 check_for_non_standard_constructs(filename, clean_lines, line, error) 1453 check_posix_threading(filename, clean_lines, line, error) 1454 check_memory_functions(filename, clean_lines, line, error) 1455 check_os_functions(filename, clean_lines, line, error) 1456 1457 for _, check_fn in ipairs(extra_check_functions or {}) do 1458 check_fn(filename, clean_lines, line, error) 1459 end 1460 end 1461 1462 local function process_file_data(filename, file_extension, lines, error, extra_check_functions) 1463 -- Add marker lines 1464 table.insert(lines, 1, '// marker so line numbers and indices both start at 1') 1465 table.insert(lines, '// marker so line numbers end in a known way') 1466 1467 local nesting_state = NestingState.new() 1468 1469 reset_nolint_suppressions() 1470 reset_known_error_suppressions() 1471 1472 local init_lines = vim.deepcopy(lines) 1473 1474 if cpplint_state.record_errors_file then 1475 local function recorded_error(filename_, linenum, category, confidence, message) 1476 if not is_error_suppressed_by_nolint(category, linenum) then 1477 local key_start = math.max(1, linenum) 1478 local key_end = math.min(#lines, linenum + 2) 1479 local key_lines = {} 1480 for i = key_start, key_end do 1481 table.insert(key_lines, lines[i]) 1482 end 1483 local err = { filename_, key_lines, category } 1484 cpplint_state.record_errors_file:write(vim.json.encode(err) .. '\n') 1485 end 1486 error(filename_, linenum, category, confidence, message) 1487 end 1488 error = recorded_error 1489 end 1490 1491 remove_multiline_comments(filename, lines, error) 1492 local clean_lines = CleansedLines.new(lines, init_lines) 1493 1494 for line = 0, clean_lines:num_lines() - 1 do 1495 process_line(filename, clean_lines, line, nesting_state, error, extra_check_functions) 1496 end 1497 1498 if file_extension == 'h' then 1499 check_for_header_guard(filename, lines, error) 1500 check_includes(filename, lines, error) 1501 if filename:match('/defs%.h$') or filename:match('_defs%.h$') then 1502 check_non_symbols(filename, lines, error) 1503 end 1504 end 1505 1506 check_for_bad_characters(filename, lines, error) 1507 end 1508 1509 local function process_file(filename, vlevel, extra_check_functions) 1510 cpplint_state:set_verbose_level(vlevel) 1511 1512 local lines 1513 1514 if filename == '-' then 1515 local stdin = io.read('*all') 1516 lines = vim.split(stdin, '\n') 1517 if cpplint_state.stdin_filename ~= '' then 1518 filename = cpplint_state.stdin_filename 1519 end 1520 else 1521 local ok, content = pcall(vim.fn.readfile, filename) 1522 if not ok then 1523 io.stderr:write("Skipping input '" .. filename .. "': Can't open for reading\n") 1524 return 1525 end 1526 lines = content 1527 end 1528 1529 -- Remove trailing '\r' 1530 for i, line in ipairs(lines) do 1531 if line:sub(-1) == '\r' then 1532 lines[i] = line:sub(1, -2) 1533 end 1534 end 1535 1536 local file_extension = filename:match('^.+%.(.+)$') or '' 1537 1538 if filename ~= '-' and not valid_extensions[file_extension] then 1539 local ext_list = {} 1540 for ext, _ in pairs(valid_extensions) do 1541 table.insert(ext_list, '.' .. ext) 1542 end 1543 io.stderr:write( 1544 'Ignoring ' .. filename .. '; only linting ' .. table.concat(ext_list, ', ') .. ' files\n' 1545 ) 1546 else 1547 process_file_data(filename, file_extension, lines, error_func, extra_check_functions) 1548 end 1549 end 1550 1551 -- Main function 1552 local function main(args) 1553 local filenames, opts = parse_arguments(args) 1554 1555 cpplint_state:set_output_format(opts.output_format) 1556 cpplint_state:set_verbose_level(opts.verbose_level) 1557 cpplint_state:set_filters(opts.filters) 1558 cpplint_state:set_counting_style(opts.counting_style) 1559 valid_extensions = {} 1560 for _, ext in ipairs(opts.extensions) do 1561 valid_extensions[ext] = true 1562 end 1563 1564 cpplint_state:suppress_errors_from(opts.suppress_errors_file) 1565 cpplint_state:record_errors_to(opts.record_errors_file) 1566 cpplint_state.stdin_filename = opts.stdin_filename 1567 1568 cpplint_state:reset_error_counts() 1569 1570 for _, filename in ipairs(filenames) do 1571 process_file(filename, cpplint_state.verbose_level) 1572 end 1573 1574 cpplint_state:print_error_counts() 1575 1576 if cpplint_state.record_errors_file then 1577 cpplint_state.record_errors_file:close() 1578 end 1579 1580 vim.cmd.cquit(cpplint_state.error_count > 0 and 1 or 0) 1581 end 1582 1583 -- Export main function 1584 main(_G.arg)