diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/scripts/checkpatch.pl | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip | |
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc
git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e
git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/scripts/checkpatch.pl')
| -rwxr-xr-x | meshmc/scripts/checkpatch.pl | 5480 |
1 files changed, 5480 insertions, 0 deletions
diff --git a/meshmc/scripts/checkpatch.pl b/meshmc/scripts/checkpatch.pl new file mode 100755 index 0000000000..43098ca2e6 --- /dev/null +++ b/meshmc/scripts/checkpatch.pl @@ -0,0 +1,5480 @@ +#!/usr/bin/env perl +# SPDX-FileCopyrightText: 2026 Project Tick +# SPDX-FileContributor: Project Tick +# SPDX-License-Identifier: GPL-3.0-or-later +# +# MeshMC checkpatch.pl - Coding Style and Convention Checker +# +# This script checks C++, header, CMake, and other source files in the +# MeshMC project for adherence to the project's coding conventions. +# +# Usage: +# ./scripts/checkpatch.pl [OPTIONS] [FILES...] +# git diff | ./scripts/checkpatch.pl --diff +# ./scripts/checkpatch.pl --git HEAD~1 +# +# Options: +# --diff Read unified diff from stdin +# --git <ref> Check changes since git ref +# --file <path> Check a specific file (can be repeated) +# --dir <path> Check all files in directory recursively +# --repository Scan the entire git repository from root +# --fix Attempt to fix simple issues in-place +# --quiet Only show errors, not warnings +# --verbose Show additional diagnostic information +# --summary Show summary of issues at end +# --color Force colored output +# --no-color Disable colored output +# --max-line-length <n> Set maximum line length (default: 120) +# --exclude <pat> Exclude files matching glob pattern +# --help Show this help message +# --version Show version information +# +# Exit codes: +# 0 - No errors found +# 1 - Errors found +# 2 - Warnings found (no errors) +# 3 - Script usage error + +use strict; +use warnings; +use utf8; +use File::Basename; +use File::Find; +use File::Spec; +use Getopt::Long qw(:config no_ignore_case bundling); +use Cwd qw(abs_path getcwd); +use POSIX qw(strftime); + +# =========================================================================== +# Version and Constants +# =========================================================================== + +our $VERSION = "1.0.0"; +our $PROGRAM = "MeshMC checkpatch"; + +# Severity levels +use constant { + SEV_ERROR => 0, + SEV_WARNING => 1, + SEV_INFO => 2, + SEV_STYLE => 3, +}; + +# File type constants +use constant { + FTYPE_CPP => 'cpp', + FTYPE_HEADER => 'header', + FTYPE_CMAKE => 'cmake', + FTYPE_UI => 'ui', + FTYPE_QRC => 'qrc', + FTYPE_OTHER => 'other', +}; + +# =========================================================================== +# Configuration and Defaults +# =========================================================================== + +my $MAX_LINE_LENGTH = 120; +my $INDENT_WIDTH = 4; +my $CMAKE_INDENT_WIDTH = 3; +my $MAX_FUNCTION_LENGTH = 500; +my $MAX_FILE_LENGTH = 3000; +my $MAX_NESTING_DEPTH = 6; +my $MAX_PARAMS_PER_FUNCTION = 8; +my $MAX_CONSECUTIVE_BLANK = 2; + +# =========================================================================== +# Global State +# =========================================================================== + +my @g_errors = (); +my @g_warnings = (); +my @g_info = (); +my $g_error_count = 0; +my $g_warn_count = 0; +my $g_info_count = 0; +my $g_file_count = 0; +my $g_line_count = 0; +my @g_current_lines = (); + +# Options +my $opt_diff = 0; +my $opt_git = ''; +my @opt_files = (); +my @opt_dirs = (); +my $opt_fix = 0; +my $opt_quiet = 0; +my $opt_verbose = 0; +my $opt_summary = 0; +my $opt_color = -1; # auto-detect +my $opt_repository = 0; +my $opt_report = ''; +my $opt_help = 0; +my $opt_version = 0; +my @opt_excludes = (); + +# ANSI color codes +my %colors = ( + 'red' => "\033[1;31m", + 'green' => "\033[1;32m", + 'yellow' => "\033[1;33m", + 'blue' => "\033[1;34m", + 'magenta' => "\033[1;35m", + 'cyan' => "\033[1;36m", + 'white' => "\033[1;37m", + 'reset' => "\033[0m", + 'bold' => "\033[1m", + 'dim' => "\033[2m", +); + +# =========================================================================== +# Naming Convention Patterns +# =========================================================================== + +# PascalCase: starts with uppercase, mixed case +my $RE_PASCAL_CASE = qr/^[A-Z][a-zA-Z0-9]*$/; + +# camelCase: starts with lowercase, mixed case +my $RE_CAMEL_CASE = qr/^[a-z][a-zA-Z0-9]*$/; + +# m_ prefix member variable: m_camelCase +my $RE_MEMBER_VAR = qr/^m_[a-z][a-zA-Z0-9]*$/; + +# UPPER_SNAKE_CASE for macros +my $RE_MACRO_CASE = qr/^[A-Z][A-Z0-9_]*$/; + +# snake_case +my $RE_SNAKE_CASE = qr/^[a-z][a-z0-9_]*$/; + +# Qt slot naming: on_widgetName_signalName +my $RE_QT_SLOT = qr/^on_[a-zA-Z]+_[a-zA-Z]+$/; + +# =========================================================================== +# License Header Templates +# =========================================================================== +# REUSE-IgnoreStart +my $SPDX_HEADER_PATTERN = qr{ + /\*\s*SPDX-FileCopyrightText:.*?\n + .*?SPDX-(?:FileContributor|FileCopyrightText):.*?\n + .*?SPDX-License-Identifier:\s*GPL-3\.0-or-later +}xs; + +my $SPDX_CMAKE_PATTERN = qr{ + \#\s*SPDX-(?:FileCopyrightText|License-Identifier): +}x; +# REUSE-IgnoreEnd +# =========================================================================== +# Known Qt Types and Macros +# =========================================================================== + +my %KNOWN_QT_TYPES = map { $_ => 1 } qw( + QString QStringList QByteArray QVariant QUrl QDir QFile QFileInfo + QDateTime QDate QTime QTimer QObject QWidget QMainWindow QDialog + QPushButton QLabel QLineEdit QTextEdit QComboBox QCheckBox + QListWidget QTreeWidget QTableWidget QMenu QMenuBar QToolBar + QAction QStatusBar QMessageBox QFileDialog QColorDialog + QLayout QVBoxLayout QHBoxLayout QGridLayout QFormLayout + QSplitter QTabWidget QStackedWidget QGroupBox QFrame + QScrollArea QDockWidget QToolBox QProgressBar QSlider + QSpinBox QDoubleSpinBox QRadioButton QButtonGroup + QMap QHash QList QVector QSet QMultiMap QMultiHash + QPair QSharedPointer QWeakPointer QScopedPointer + QJsonDocument QJsonObject QJsonArray QJsonValue QJsonParseError + QProcess QThread QMutex QMutexLocker QWaitCondition + QNetworkAccessManager QNetworkRequest QNetworkReply + QSettings QStandardPaths QCoreApplication QApplication + QPixmap QIcon QImage QPainter QColor QFont QPen QBrush + QPoint QPointF QSize QSizeF QRect QRectF + QModelIndex QAbstractListModel QAbstractTableModel QAbstractItemModel + QSortFilterProxyModel QItemSelectionModel QStyledItemDelegate + QDomDocument QDomElement QDomNode QDomNodeList + QRegularExpression QRegularExpressionMatch + QTextStream QDataStream QBuffer QIODevice + QEvent QKeyEvent QMouseEvent QResizeEvent QCloseEvent + QSignalMapper QValidator QIntValidator QDoubleValidator + QTranslator QLocale QLatin1String QStringLiteral + QTest +); + +my %KNOWN_QT_MACROS = map { $_ => 1 } qw( + Q_OBJECT Q_PROPERTY Q_ENUM Q_FLAG Q_DECLARE_FLAGS Q_DECLARE_METATYPE + Q_INVOKABLE Q_SLOT Q_SIGNAL Q_EMIT Q_UNUSED Q_ASSERT Q_ASSERT_X + Q_DISABLE_COPY Q_DISABLE_MOVE Q_DISABLE_COPY_MOVE + SIGNAL SLOT + QTEST_GUILESS_MAIN QTEST_MAIN QTEST_APPLESS_MAIN + QCOMPARE QVERIFY QVERIFY2 QFETCH QSKIP QFAIL QEXPECT_FAIL + QBENCHMARK +); + +# Known MeshMC macros +my %KNOWN_PROJECT_MACROS = map { $_ => 1 } qw( + APPLICATION STRINGIFY TOSTRING MACOS_HINT + WIN32_LEAN_AND_MEAN +); + +# =========================================================================== +# File Extension Mappings +# =========================================================================== + +my %EXT_TO_FTYPE = ( + '.cpp' => FTYPE_CPP, + '.cxx' => FTYPE_CPP, + '.cc' => FTYPE_CPP, + '.c' => FTYPE_CPP, + '.h' => FTYPE_HEADER, + '.hpp' => FTYPE_HEADER, + '.hxx' => FTYPE_HEADER, + '.ui' => FTYPE_UI, + '.qrc' => FTYPE_QRC, +); + +my %CMAKE_FILES = map { $_ => 1 } qw( + CMakeLists.txt +); + +# Source file extensions to check +my @SOURCE_EXTENSIONS = qw(.cpp .cxx .cc .c .h .hpp .hxx); + +# =========================================================================== +# Excluded Directories and Files +# =========================================================================== + +my @DEFAULT_EXCLUDES = ( + 'build/', + 'libraries/', + '.git/', + 'CMakeFiles/', + 'Testing/', + 'jars/', + 'Debug/', + 'Release/', + 'RelWithDebInfo/', + 'moc_*', + 'ui_*', + 'qrc_*', + '*_autogen*', +); + +# =========================================================================== +# Main Entry Point +# =========================================================================== + +sub main { + parse_options(); + + if ($opt_help) { + print_usage(); + exit(0); + } + + if ($opt_version) { + print "$PROGRAM v$VERSION\n"; + exit(0); + } + + # Determine color support + if ($opt_color == -1) { + $opt_color = (-t STDOUT) ? 1 : 0; + } + + if ($opt_diff) { + process_diff_from_stdin(); + } elsif ($opt_git) { + process_git_diff($opt_git); + } elsif (@opt_files) { + foreach my $file (@opt_files) { + process_file($file); + } + } elsif (@opt_dirs) { + foreach my $dir (@opt_dirs) { + process_directory($dir); + } + } elsif ($opt_repository) { + my $root = find_git_root(); + if (!$root) { + print STDERR "ERROR: --repository used but no git repository found.\n"; + exit(3); + } + print "Scanning repository at: $root\n" if $opt_verbose; + process_directory($root); + } else { + # No input specified: show help + print_usage(); + exit(0); + } + + if ($opt_summary) { + print_summary(); + } + + if ($opt_report) { + write_report($opt_report); + } + + # Exit code based on results + # Warnings alone do not cause a non-zero exit + if ($g_error_count > 0) { + exit(1); + } + exit(0); +} + +# =========================================================================== +# Option Parsing +# =========================================================================== + +sub parse_options { + GetOptions( + 'diff' => \$opt_diff, + 'git=s' => \$opt_git, + 'file=s' => \@opt_files, + 'dir=s' => \@opt_dirs, + 'repository|repo|R' => \$opt_repository, + 'report=s' => \$opt_report, + 'fix' => \$opt_fix, + 'quiet|q' => \$opt_quiet, + 'verbose|v' => \$opt_verbose, + 'summary|s' => \$opt_summary, + 'color' => sub { $opt_color = 1; }, + 'no-color' => sub { $opt_color = 0; }, + 'max-line-length=i' => \$MAX_LINE_LENGTH, + 'exclude=s' => \@opt_excludes, + 'help|h' => \$opt_help, + 'version|V' => \$opt_version, + ) or do { + print_usage(); + exit(3); + }; + + # Remaining arguments are files + push @opt_files, @ARGV; +} + +sub print_usage { + print <<'USAGE'; +MeshMC checkpatch.pl - Coding Style and Convention Checker + +Usage: + checkpatch.pl [OPTIONS] [FILES...] + git diff | checkpatch.pl --diff + checkpatch.pl --git HEAD~1 + +Options: + --diff Read unified diff from stdin + --git <ref> Check changes since git ref + --file <path> Check a specific file (can be repeated) + --dir <path> Check all files in directory recursively + --repository, -R Scan the entire git repository from its root + --report <file> Write report to file (.txt, .json, .html, .csv) + --fix Attempt to fix simple issues in-place + --quiet, -q Only show errors, not warnings + --verbose, -v Show additional diagnostic information + --summary, -s Show summary of issues at end + --color Force colored output + --no-color Disable colored output + --max-line-length <n> Set maximum line length (default: 120) + --exclude <pat> Exclude files matching glob pattern + --help, -h Show this help message + --version, -V Show version information + +Checked conventions: + - 4-space indentation (no tabs) for C++/header files + - 3-space indentation for CMake files + - K&R brace placement style + - PascalCase class names, camelCase methods, m_ member variables + - #pragma once header guards + - SPDX license headers + - Qt conventions (signal/slot, Q_OBJECT, etc.) + - Line length limits (default 120 chars) + - Trailing whitespace + - Proper include ordering + - const correctness hints + - Memory management patterns + - Other MeshMC-specific conventions + +Exit codes: + 0 - No errors found + 1 - Errors found + 2 - Warnings found (no errors) + 3 - Script usage error + +USAGE +} + +# =========================================================================== +# Color Output Helpers +# =========================================================================== + +sub colorize { + my ($color_name, $text) = @_; + return $text unless $opt_color; + return ($colors{$color_name} // '') . $text . $colors{'reset'}; +} + +sub severity_label { + my ($severity) = @_; + if ($severity == SEV_ERROR) { + return colorize('red', 'ERROR'); + } elsif ($severity == SEV_WARNING) { + return colorize('yellow', 'WARNING'); + } elsif ($severity == SEV_INFO) { + return colorize('cyan', 'INFO'); + } elsif ($severity == SEV_STYLE) { + return colorize('magenta', 'STYLE'); + } + return 'UNKNOWN'; +} + +# =========================================================================== +# Reporting Functions +# =========================================================================== + +sub report { + my ($file, $line, $severity, $rule, $message) = @_; + + # Support NOLINT suppression comments in source lines + if ($line > 0 && $line <= scalar @g_current_lines) { + my $src = $g_current_lines[$line - 1]; + if ($src =~ m{//\s*NOLINT\b(?:\(([^)]+)\))?}) { + my $nolint_rules = $1; + if (!defined $nolint_rules || $nolint_rules =~ /\b\Q$rule\E\b/i) { + return; # Suppressed by NOLINT + } + } + } + + # Always track counts and store issues regardless of display mode + if ($severity == SEV_ERROR) { + $g_error_count++; + push @g_errors, { + file => $file, + line => $line, + rule => $rule, + message => $message, + }; + } elsif ($severity == SEV_WARNING) { + $g_warn_count++; + push @g_warnings, { + file => $file, + line => $line, + rule => $rule, + message => $message, + }; + } else { + $g_info_count++; + push @g_info, { + file => $file, + line => $line, + rule => $rule, + message => $message, + }; + } + + # Determine whether to print to stdout + return if ($opt_quiet && $severity > SEV_ERROR); + return if (!$opt_verbose && $severity >= SEV_INFO); + + my $label = severity_label($severity); + my $location = colorize('bold', "$file:$line"); + my $rule_tag = colorize('dim', "[$rule]"); + + print "$location: $label: $message $rule_tag\n"; +} + +sub print_summary { + print "\n"; + print colorize('bold', "=" x 60) . "\n"; + print colorize('bold', " MeshMC checkpatch Summary") . "\n"; + print colorize('bold', "=" x 60) . "\n"; + print " Files checked: $g_file_count\n"; + print " Lines checked: $g_line_count\n"; + print " Errors: " . colorize('red', $g_error_count) . "\n"; + print " Warnings: " . colorize('yellow', $g_warn_count) . "\n"; + if ($opt_verbose) { + print " Info: " . colorize('cyan', $g_info_count) . "\n"; + } + print colorize('bold', "=" x 60) . "\n"; + + if ($g_error_count == 0 && $g_warn_count == 0) { + print colorize('green', " All checks passed!") . "\n"; + } + print "\n"; +} + +# =========================================================================== +# Report Generation +# =========================================================================== + +sub write_report { + my ($report_path) = @_; + + my @all_issues = (); + foreach my $e (@g_errors) { + push @all_issues, { %$e, severity => 'ERROR' }; + } + foreach my $w (@g_warnings) { + push @all_issues, { %$w, severity => 'WARNING' }; + } + foreach my $info (@g_info) { + push @all_issues, { %$info, severity => 'INFO' }; + } + + # Sort issues by file, then line number + @all_issues = sort { + $a->{file} cmp $b->{file} || $a->{line} <=> $b->{line} + } @all_issues; + + # Determine format from file extension + my $format = 'txt'; + if ($report_path =~ /\.json$/i) { + $format = 'json'; + } elsif ($report_path =~ /\.html?$/i) { + $format = 'html'; + } elsif ($report_path =~ /\.csv$/i) { + $format = 'csv'; + } + + if ($format eq 'json') { + write_report_json($report_path, \@all_issues); + } elsif ($format eq 'html') { + write_report_html($report_path, \@all_issues); + } elsif ($format eq 'csv') { + write_report_csv($report_path, \@all_issues); + } else { + write_report_txt($report_path, \@all_issues); + } + + print "Report written to: $report_path\n"; +} + +sub write_report_txt { + my ($path, $issues) = @_; + + open(my $fh, '>:encoding(UTF-8)', $path) or die "Cannot write report to $path: $!\n"; + + print $fh "=" x 70, "\n"; + print $fh " MeshMC checkpatch Report\n"; + print $fh " Generated: " . POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime) . "\n"; + print $fh "=" x 70, "\n\n"; + + print $fh "Summary:\n"; + print $fh " Files checked: $g_file_count\n"; + print $fh " Lines checked: $g_line_count\n"; + print $fh " Errors: $g_error_count\n"; + print $fh " Warnings: $g_warn_count\n"; + print $fh " Info: $g_info_count\n"; + print $fh "\n"; + + if (@$issues == 0) { + print $fh "All checks passed! No issues found.\n"; + } else { + print $fh "-" x 70, "\n"; + print $fh " Issues (" . scalar(@$issues) . " total)\n"; + print $fh "-" x 70, "\n\n"; + + my $current_file = ''; + foreach my $issue (@$issues) { + if ($issue->{file} ne $current_file) { + $current_file = $issue->{file}; + print $fh "--- $current_file ---\n"; + } + printf $fh " Line %d: %s [%s] %s\n", + $issue->{line}, $issue->{severity}, $issue->{rule}, $issue->{message}; + } + } + + print $fh "\n", "=" x 70, "\n"; + close($fh); +} + +sub write_report_json { + my ($path, $issues) = @_; + + open(my $fh, '>:encoding(UTF-8)', $path) or die "Cannot write report to $path: $!\n"; + + print $fh "{\n"; + print $fh " \"generator\": \"$PROGRAM\",\n"; + print $fh " \"version\": \"$VERSION\",\n"; + print $fh " \"timestamp\": \"" . POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime) . "\",\n"; + print $fh " \"summary\": {\n"; + print $fh " \"files_checked\": $g_file_count,\n"; + print $fh " \"lines_checked\": $g_line_count,\n"; + print $fh " \"errors\": $g_error_count,\n"; + print $fh " \"warnings\": $g_warn_count,\n"; + print $fh " \"info\": $g_info_count\n"; + print $fh " },\n"; + print $fh " \"issues\": [\n"; + + for (my $i = 0; $i < scalar @$issues; $i++) { + my $issue = $issues->[$i]; + my $comma = ($i < scalar(@$issues) - 1) ? ',' : ''; + + # Escape JSON strings + my $file_j = json_escape($issue->{file}); + my $msg_j = json_escape($issue->{message}); + my $rule_j = json_escape($issue->{rule}); + my $sev_j = json_escape($issue->{severity}); + + print $fh " {\n"; + print $fh " \"file\": \"$file_j\",\n"; + print $fh " \"line\": $issue->{line},\n"; + print $fh " \"severity\": \"$sev_j\",\n"; + print $fh " \"rule\": \"$rule_j\",\n"; + print $fh " \"message\": \"$msg_j\"\n"; + print $fh " }$comma\n"; + } + + print $fh " ]\n"; + print $fh "}\n"; + + close($fh); +} + +sub json_escape { + my ($str) = @_; + return '' unless defined $str; + $str =~ s/\\/\\\\/g; + $str =~ s/"/\\"/g; + $str =~ s/\n/\\n/g; + $str =~ s/\r/\\r/g; + $str =~ s/\t/\\t/g; + # Escape control characters + $str =~ s/([\x00-\x1f])/sprintf("\\u%04x", ord($1))/ge; + return $str; +} + +sub write_report_html { + my ($path, $issues) = @_; + + open(my $fh, '>:encoding(UTF-8)', $path) or die "Cannot write report to $path: $!\n"; + + my $timestamp = POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime); + my $total = scalar @$issues; + + print $fh <<"HTML"; +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>MeshMC checkpatch Report</title> +<style> + * { margin: 0; padding: 0; box-sizing: border-box; } + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0d1117; color: #c9d1d9; line-height: 1.6; padding: 2rem; } + .container { max-width: 1200px; margin: 0 auto; } + h1 { color: #58a6ff; margin-bottom: 0.5rem; font-size: 1.8rem; } + .meta { color: #8b949e; margin-bottom: 1.5rem; font-size: 0.9rem; } + .summary { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; } + .stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; + padding: 1rem 1.5rem; min-width: 140px; } + .stat .value { font-size: 2rem; font-weight: bold; } + .stat .label { color: #8b949e; font-size: 0.85rem; } + .stat.errors .value { color: #f85149; } + .stat.warnings .value { color: #d29922; } + .stat.info .value { color: #58a6ff; } + .stat.files .value { color: #3fb950; } + .filters { margin-bottom: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap; } + .filters button { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; + border-radius: 6px; padding: 0.4rem 1rem; cursor: pointer; + font-size: 0.85rem; transition: all 0.2s; } + .filters button:hover { background: #30363d; } + .filters button.active { background: #388bfd26; border-color: #58a6ff; color: #58a6ff; } + table { width: 100%; border-collapse: collapse; background: #161b22; + border: 1px solid #30363d; border-radius: 8px; overflow: hidden; } + th { background: #21262d; text-align: left; padding: 0.75rem 1rem; + color: #8b949e; font-size: 0.85rem; text-transform: uppercase; + border-bottom: 1px solid #30363d; } + td { padding: 0.6rem 1rem; border-bottom: 1px solid #30363d1a; + font-size: 0.9rem; vertical-align: top; } + tr:hover td { background: #1c2128; } + .sev { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; + font-size: 0.75rem; font-weight: 600; text-transform: uppercase; } + .sev.error { background: #f8514926; color: #f85149; } + .sev.warning { background: #d2992226; color: #d29922; } + .sev.info { background: #58a6ff26; color: #58a6ff; } + .rule { color: #bc8cff; font-family: monospace; font-size: 0.85rem; } + .file { color: #58a6ff; font-family: monospace; font-size: 0.85rem; } + .line-num { color: #8b949e; font-family: monospace; } + .message { color: #c9d1d9; } + .passed { text-align: center; padding: 3rem; color: #3fb950; font-size: 1.3rem; } + .passed::before { content: "\\2713 "; font-size: 2rem; } + .footer { margin-top: 2rem; color: #484f58; font-size: 0.8rem; text-align: center; } +</style> +</head> +<body> +<div class="container"> +<h1>MeshMC checkpatch Report</h1> +<div class="meta">Generated: $timestamp | $PROGRAM v$VERSION</div> +<div class="summary"> + <div class="stat files"><div class="value">$g_file_count</div><div class="label">Files Checked</div></div> + <div class="stat"><div class="value">$g_line_count</div><div class="label">Lines Checked</div></div> + <div class="stat errors"><div class="value">$g_error_count</div><div class="label">Errors</div></div> + <div class="stat warnings"><div class="value">$g_warn_count</div><div class="label">Warnings</div></div> + <div class="stat info"><div class="value">$g_info_count</div><div class="label">Info</div></div> +</div> +HTML + + if ($total == 0) { + print $fh "<div class=\"passed\">All checks passed! No issues found.</div>\n"; + } else { + print $fh <<"FILTER_JS"; +<div class="filters"> + <button class="active" onclick="filterRows('all', this)">All ($total)</button> + <button onclick="filterRows('error', this)">Errors ($g_error_count)</button> + <button onclick="filterRows('warning', this)">Warnings ($g_warn_count)</button> + <button onclick="filterRows('info', this)">Info ($g_info_count)</button> +</div> +<table> +<thead> +<tr><th>File</th><th>Line</th><th>Severity</th><th>Rule</th><th>Message</th></tr> +</thead> +<tbody> +FILTER_JS + + foreach my $issue (@$issues) { + my $sev_class = lc($issue->{severity}); + my $file_h = html_escape($issue->{file}); + my $msg_h = html_escape($issue->{message}); + my $rule_h = html_escape($issue->{rule}); + print $fh "<tr data-severity=\"$sev_class\">"; + print $fh "<td class=\"file\">$file_h</td>"; + print $fh "<td class=\"line-num\">$issue->{line}</td>"; + print $fh "<td><span class=\"sev $sev_class\">$issue->{severity}</span></td>"; + print $fh "<td class=\"rule\">$rule_h</td>"; + print $fh "<td class=\"message\">$msg_h</td>"; + print $fh "</tr>\n"; + } + + print $fh <<"TABLE_END"; +</tbody> +</table> +<script> +function filterRows(severity, btn) { + document.querySelectorAll('.filters button').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + document.querySelectorAll('tbody tr').forEach(row => { + if (severity === 'all' || row.dataset.severity === severity) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + }); +} +</script> +TABLE_END + } + + print $fh "<div class=\"footer\">Generated by $PROGRAM v$VERSION</div>\n"; + print $fh "</div>\n</body>\n</html>\n"; + + close($fh); +} + +sub html_escape { + my ($str) = @_; + return '' unless defined $str; + $str =~ s/&/&/g; + $str =~ s/</</g; + $str =~ s/>/>/g; + $str =~ s/"/"/g; + $str =~ s/'/'/g; + return $str; +} + +sub write_report_csv { + my ($path, $issues) = @_; + + open(my $fh, '>:encoding(UTF-8)', $path) or die "Cannot write report to $path: $!\n"; + + # CSV header + print $fh "File,Line,Severity,Rule,Message\n"; + + foreach my $issue (@$issues) { + my $file = csv_escape($issue->{file}); + my $msg = csv_escape($issue->{message}); + my $rule = csv_escape($issue->{rule}); + my $sev = csv_escape($issue->{severity}); + print $fh "$file,$issue->{line},$sev,$rule,$msg\n"; + } + + close($fh); +} + +sub csv_escape { + my ($str) = @_; + return '""' unless defined $str; + # If field contains comma, quote, or newline, wrap in quotes + if ($str =~ /[",\n\r]/) { + $str =~ s/"/""/g; + return "\"$str\""; + } + return $str; +} + +# =========================================================================== +# File Discovery and Filtering +# =========================================================================== + +sub should_exclude { + my ($filepath) = @_; + + # Check default excludes + foreach my $pattern (@DEFAULT_EXCLUDES) { + my $regex = glob_to_regex($pattern); + return 1 if $filepath =~ /$regex/; + } + + # Check user excludes + foreach my $pattern (@opt_excludes) { + my $regex = glob_to_regex($pattern); + return 1 if $filepath =~ /$regex/; + } + + return 0; +} + +sub glob_to_regex { + my ($glob) = @_; + my $regex = quotemeta($glob); + $regex =~ s/\\\*\\\*/.*/g; # ** -> .* + $regex =~ s/\\\*([^.])/[^\/]*$1/g; # * -> [^/]* + $regex =~ s/\\\*$/[^\/]*/g; # trailing * -> [^/]* + $regex =~ s/\\\?/./g; # ? -> . + return $regex; +} + +sub get_file_type { + my ($filepath) = @_; + my $basename = basename($filepath); + + # Check CMake files + if ($basename eq 'CMakeLists.txt' || $filepath =~ /\.cmake$/i) { + return FTYPE_CMAKE; + } + + # Check by extension + my ($name, $dir, $ext) = fileparse($filepath, qr/\.[^.]*/); + $ext = lc($ext); + + return $EXT_TO_FTYPE{$ext} // FTYPE_OTHER; +} + +sub is_source_file { + my ($filepath) = @_; + my $ftype = get_file_type($filepath); + return ($ftype eq FTYPE_CPP || $ftype eq FTYPE_HEADER || $ftype eq FTYPE_CMAKE); +} + +# =========================================================================== +# Directory Processing +# =========================================================================== + +sub find_git_root { + my $output = `git rev-parse --show-toplevel 2>/dev/null`; + chomp $output if $output; + return $output if $output && -d $output; + return undef; +} + +sub process_directory { + my ($dir) = @_; + + $dir = abs_path($dir) unless File::Spec->file_name_is_absolute($dir); + + find({ + wanted => sub { + my $filepath = $File::Find::name; + return unless -f $filepath; + return if should_exclude($filepath); + return unless is_source_file($filepath); + process_file($filepath); + }, + no_chdir => 1, + }, $dir); +} + +# =========================================================================== +# Diff Processing +# =========================================================================== + +sub process_diff_from_stdin { + if (-t STDIN) { + print STDERR "WARNING: --diff reads from stdin, but stdin is a terminal.\n"; + print STDERR "Usage: git diff | $0 --diff\n"; + print STDERR " $0 --diff < file.patch\n"; + print STDERR "Press Ctrl+D to send EOF, or Ctrl+C to cancel.\n\n"; + } + my @diff_lines = <STDIN>; + process_diff_content(\@diff_lines); +} + +sub process_git_diff { + my ($ref) = @_; + + # Sanitize ref to prevent command injection + if ($ref =~ /[;&|`\$\(\)\{\}\[\]<>!\\]/) { + die "ERROR: Invalid git ref: contains shell metacharacters\n"; + } + if ($ref =~ /\s/) { + die "ERROR: Invalid git ref: contains whitespace\n"; + } + + my @diff_lines = `git diff "$ref" -- '*.cpp' '*.h' '*.hpp' 'CMakeLists.txt' '*.cmake' 2>&1`; + if ($? != 0) { + die "ERROR: git diff failed: @diff_lines\n"; + } + + process_diff_content(\@diff_lines); +} + +sub process_diff_content { + my ($lines_ref) = @_; + + # First pass: parse diff to collect changed line numbers per file + my %file_changes; # filename => { line_num => 1, ... } + my $current_file = ''; + my $current_line = 0; + + foreach my $line (@$lines_ref) { + chomp $line; + + # New file in diff: "diff --git a/foo.cpp b/foo.cpp" + if ($line =~ /^diff --git a\/.+? b\/(.+)/) { + $current_file = $1; + $current_line = 0; + $file_changes{$current_file} //= {}; + next; + } + + # Also catch "+++ b/foo.cpp" for non-git diffs + if ($line =~ /^\+\+\+ b\/(.+)/) { + $current_file = $1; + $file_changes{$current_file} //= {}; + next; + } + + # Hunk header: @@ -old,count +new,count @@ + if ($line =~ /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/) { + $current_line = $1 - 1; + next; + } + + # Skip other header lines + next if $line =~ /^(?:---|\+\+\+|index |new file|deleted file|old mode|new mode|similarity|rename|Binary)/; + next unless $current_file; + + # Added line — this is a changed line + if ($line =~ /^\+/) { + $current_line++; + $file_changes{$current_file}{$current_line} = 1; + } + # Context line — exists in new file, not changed + elsif ($line =~ /^ /) { + $current_line++; + } + # Removed line — don't increment (doesn't exist in new file) + elsif ($line =~ /^-/) { + # skip + } + } + + # Second pass: for each changed file, read the full file from disk + # and check it with only diff-changed lines flagged + foreach my $file (sort keys %file_changes) { + my $filepath = $file; + + # Resolve to absolute path if the file exists relative to cwd + if (!File::Spec->file_name_is_absolute($filepath)) { + my $abs = File::Spec->rel2abs($filepath); + $filepath = $abs if -f $abs; + } + + unless (-f $filepath && -r $filepath) { + # File might have been deleted in the diff + next; + } + + # Skip binary files + next if -B $filepath; + + # Skip excluded files + next if should_exclude($filepath); + + # Skip non-source files + next unless is_source_file($filepath); + + # Read the full file from disk + open(my $fh, '<:encoding(UTF-8)', $filepath) or do { + report($filepath, 0, SEV_ERROR, 'FILE_READ', + "Cannot open file: $!"); + next; + }; + my @lines = <$fh>; + close($fh); + chomp(@lines); + + $g_file_count++; + $g_line_count += scalar @lines; + + # Pass the full file content but only mark diff-changed lines + my %changed = %{$file_changes{$file}}; + @g_current_lines = @lines; + check_file_content($filepath, \@lines, \%changed); + } +} + +# =========================================================================== +# File Processing +# =========================================================================== + +sub process_file { + my ($filepath) = @_; + + # Ensure the file exists and is readable + unless (-f $filepath && -r $filepath) { + report($filepath, 0, SEV_ERROR, 'FILE_ACCESS', + "Cannot read file"); + return; + } + + # Skip binary files + if (-B $filepath) { + return; + } + + # Read file content + open(my $fh, '<:encoding(UTF-8)', $filepath) or do { + report($filepath, 0, SEV_ERROR, 'FILE_READ', + "Cannot open file: $!"); + return; + }; + my @lines = <$fh>; + close($fh); + + chomp(@lines); + + $g_file_count++; + $g_line_count += scalar @lines; + + # Store current file lines for NOLINT support in report() + @g_current_lines = @lines; + + # All lines are considered "changed" when checking a file directly + my %all_lines = map { ($_ + 1) => 1 } (0 .. $#lines); + check_file_content($filepath, \@lines, \%all_lines); +} + +# =========================================================================== +# Master Check Dispatcher +# =========================================================================== + +sub check_file_content { + my ($filepath, $lines_ref, $changed_lines_ref) = @_; + + my $ftype = get_file_type($filepath); + + if ($opt_verbose) { + print colorize('dim', "Checking: $filepath ($ftype)") . "\n"; + } + + # Common checks for all file types + check_trailing_whitespace($filepath, $lines_ref, $changed_lines_ref); + check_line_endings($filepath, $lines_ref, $changed_lines_ref); + check_file_ending_newline($filepath, $lines_ref); + check_consecutive_blank_lines($filepath, $lines_ref, $changed_lines_ref); + + # Type-specific checks + if ($ftype eq FTYPE_CPP || $ftype eq FTYPE_HEADER) { + check_cpp_conventions($filepath, $lines_ref, $changed_lines_ref, $ftype); + } elsif ($ftype eq FTYPE_CMAKE) { + check_cmake_conventions($filepath, $lines_ref, $changed_lines_ref); + } +} + +# =========================================================================== +# Common Checks (All File Types) +# =========================================================================== + +sub check_trailing_whitespace { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + if ($line =~ /[ \t]+$/) { + report($filepath, $linenum, SEV_WARNING, 'TRAILING_WHITESPACE', + "Trailing whitespace detected"); + + if ($opt_fix) { + $lines_ref->[$i] =~ s/[ \t]+$//; + } + } + } +} + +sub check_line_endings { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + if ($line =~ /\r$/) { + report($filepath, $linenum, SEV_ERROR, 'CRLF_LINE_ENDING', + "Windows-style (CRLF) line ending detected; use Unix-style (LF)"); + + if ($opt_fix) { + $lines_ref->[$i] =~ s/\r$//; + } + } + } +} + +sub check_file_ending_newline { + my ($filepath, $lines_ref) = @_; + + return unless @$lines_ref; + + # Check if file ends with a newline (the last element after chomp should be defined) + # We actually read the raw file to check this + if (open(my $fh, '<:raw', $filepath)) { + seek($fh, -1, 2); + my $last_char; + read($fh, $last_char, 1); + close($fh); + + if (defined $last_char && $last_char ne "\n") { + report($filepath, scalar @$lines_ref, SEV_WARNING, 'NO_FINAL_NEWLINE', + "File does not end with a newline"); + } + } +} + +sub check_consecutive_blank_lines { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $blank_count = 0; + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + if ($line =~ /^\s*$/) { + $blank_count++; + if ($blank_count > $MAX_CONSECUTIVE_BLANK && exists $changed_ref->{$linenum}) { + report($filepath, $linenum, SEV_WARNING, 'CONSECUTIVE_BLANK_LINES', + "More than $MAX_CONSECUTIVE_BLANK consecutive blank lines"); + } + } else { + $blank_count = 0; + } + } +} + +# =========================================================================== +# C++ and Header File Checks +# =========================================================================== + +sub check_cpp_conventions { + my ($filepath, $lines_ref, $changed_ref, $ftype) = @_; + + # File-level checks + check_license_header($filepath, $lines_ref); + check_pragma_once($filepath, $lines_ref, $ftype); + check_include_ordering($filepath, $lines_ref); + check_file_length($filepath, $lines_ref); + + # Line-level checks + check_indentation($filepath, $lines_ref, $changed_ref); + check_line_length($filepath, $lines_ref, $changed_ref); + check_tab_usage($filepath, $lines_ref, $changed_ref); + check_brace_style($filepath, $lines_ref, $changed_ref); + check_naming_conventions($filepath, $lines_ref, $changed_ref); + check_pointer_alignment($filepath, $lines_ref, $changed_ref); + check_spacing_conventions($filepath, $lines_ref, $changed_ref); + check_qt_conventions($filepath, $lines_ref, $changed_ref); + check_include_style($filepath, $lines_ref, $changed_ref); + check_comment_style($filepath, $lines_ref, $changed_ref); + check_control_flow_style($filepath, $lines_ref, $changed_ref); + check_string_usage($filepath, $lines_ref, $changed_ref); + check_memory_patterns($filepath, $lines_ref, $changed_ref); + check_const_correctness($filepath, $lines_ref, $changed_ref); + check_enum_style($filepath, $lines_ref, $changed_ref); + check_constructor_patterns($filepath, $lines_ref, $changed_ref); + check_function_length($filepath, $lines_ref); + check_nesting_depth($filepath, $lines_ref, $changed_ref); + check_deprecated_patterns($filepath, $lines_ref, $changed_ref); + check_security_patterns($filepath, $lines_ref, $changed_ref); + check_test_conventions($filepath, $lines_ref, $changed_ref); + check_exception_patterns($filepath, $lines_ref, $changed_ref); + check_virtual_destructor($filepath, $lines_ref, $changed_ref); + check_override_keyword($filepath, $lines_ref, $changed_ref); + check_auto_usage($filepath, $lines_ref, $changed_ref); + check_lambda_style($filepath, $lines_ref, $changed_ref); + check_switch_case_style($filepath, $lines_ref, $changed_ref); + check_class_member_ordering($filepath, $lines_ref, $changed_ref); + check_todo_fixme($filepath, $lines_ref, $changed_ref); + check_debug_statements($filepath, $lines_ref, $changed_ref); + check_header_self_containment($filepath, $lines_ref, $ftype); + check_multiple_statements_per_line($filepath, $lines_ref, $changed_ref); + check_magic_numbers($filepath, $lines_ref, $changed_ref); + check_using_namespace($filepath, $lines_ref, $changed_ref); +} + +# =========================================================================== +# License Header Check +# =========================================================================== +# REUSE-IgnoreStart +sub check_license_header { + my ($filepath, $lines_ref) = @_; + + return unless @$lines_ref; + + # Join first 30 lines for header check + my $header_text = join("\n", @{$lines_ref}[0 .. min(29, $#$lines_ref)]); + + # Check for SPDX header + unless ($header_text =~ /SPDX-License-Identifier/) { + report($filepath, 1, SEV_ERROR, 'MISSING_SPDX_HEADER', + "Missing SPDX license header (expected SPDX-License-Identifier)"); + return; + } + + # Check for GPL-3.0-or-later (project standard) + unless ($header_text =~ /SPDX-License-Identifier:\s*GPL-3\.0-or-later/ + || $header_text =~ /SPDX-License-Identifier:\s*(?:Apache-2\.0|LGPL-[23]\.\d|MIT|BSD)/ + || $header_text =~ /SPDX-License-Identifier:\s*\S+/) { + report($filepath, 1, SEV_WARNING, 'INVALID_SPDX_IDENTIFIER', + "SPDX-License-Identifier found but could not parse license type"); + } + + # Check for SPDX-FileCopyrightText + unless ($header_text =~ /SPDX-FileCopyrightText:/) { + report($filepath, 1, SEV_WARNING, 'MISSING_COPYRIGHT', + "Missing SPDX-FileCopyrightText in header"); + } + + # Check header is in a block comment + unless ($header_text =~ m{^\s*/\*} || $header_text =~ m{^\s*//}) { + report($filepath, 1, SEV_WARNING, 'HEADER_NOT_IN_COMMENT', + "SPDX header should be in a comment block (/* */ or //)"); + } +} +# REUSE-IgnoreEnd +# =========================================================================== +# #pragma once Check +# =========================================================================== + +sub check_pragma_once { + my ($filepath, $lines_ref, $ftype) = @_; + + return unless $ftype eq FTYPE_HEADER; + return unless @$lines_ref; + + my $found_pragma = 0; + my $found_ifndef = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + if ($line =~ /^\s*#\s*pragma\s+once\b/) { + $found_pragma = 1; + last; + } + if ($line =~ /^\s*#\s*ifndef\s+\w+_H/) { + $found_ifndef = 1; + } + + # Only check first 40 lines (past the license header) + last if $i > 40; + } + + if (!$found_pragma) { + if ($found_ifndef) { + report($filepath, 1, SEV_WARNING, 'USE_PRAGMA_ONCE', + "Use '#pragma once' instead of #ifndef header guards (project convention)"); + } else { + report($filepath, 1, SEV_ERROR, 'MISSING_HEADER_GUARD', + "Header file missing '#pragma once' guard"); + } + } +} + +# =========================================================================== +# Include Ordering Check +# =========================================================================== + +sub check_include_ordering { + my ($filepath, $lines_ref) = @_; + + my @include_groups = (); + my $current_group = []; + my $last_was_include = 0; + my $pragma_found = 0; + my $in_header_section = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + # Skip until we find #pragma once + if ($line =~ /^\s*#\s*pragma\s+once/) { + $pragma_found = 1; + $in_header_section = 1; + next; + } + + next unless $in_header_section; + + # Stop at first non-include, non-blank, non-comment line + if ($line !~ /^\s*$/ && $line !~ /^\s*#\s*include/ && $line !~ /^\s*\/\//) { + last if $last_was_include || @$current_group; + } + + if ($line =~ /^\s*#\s*include\s*[<"](.+?)[>"]/) { + push @$current_group, { + path => $1, + line => $i + 1, + system => ($line =~ /</) ? 1 : 0, + raw => $line, + }; + $last_was_include = 1; + } elsif ($line =~ /^\s*$/ && $last_was_include) { + # Blank line separates include groups + if (@$current_group) { + push @include_groups, $current_group; + $current_group = []; + } + $last_was_include = 0; + } + } + + # Push last group + if (@$current_group) { + push @include_groups, $current_group; + } + + # Check for proper grouping within each group + foreach my $group (@include_groups) { + check_include_group_sorting($filepath, $group); + } +} + +sub check_include_group_sorting { + my ($filepath, $group) = @_; + + return unless @$group > 1; + + # Check if includes within a group are alphabetically sorted + for (my $i = 1; $i < scalar @$group; $i++) { + my $prev_path = $group->[$i-1]->{path}; + my $curr_path = $group->[$i]->{path}; + + # Skip entries with undefined path (safety guard) + next unless defined $prev_path && defined $curr_path; + + my $prev = lc($prev_path); + my $curr = lc($curr_path); + + # Only warn within the same type (system vs local) + if ($group->[$i-1]->{system} == $group->[$i]->{system}) { + if ($curr lt $prev) { + report($filepath, $group->[$i]->{line}, SEV_INFO, 'INCLUDE_ORDER', + "Include '$group->[$i]->{path}' should come before '$group->[$i-1]->{path}' (alphabetical order)"); + } + } + } +} + +# =========================================================================== +# File Length Check +# =========================================================================== + +sub check_file_length { + my ($filepath, $lines_ref) = @_; + + my $line_count = scalar @$lines_ref; + if ($line_count > $MAX_FILE_LENGTH) { + report($filepath, $line_count, SEV_WARNING, 'FILE_TOO_LONG', + "File has $line_count lines (recommended maximum: $MAX_FILE_LENGTH). Consider splitting."); + } +} + +# =========================================================================== +# Indentation Check (4 spaces for C++, no tabs) +# =========================================================================== + +sub check_indentation { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_multiline_comment = 0; + my $in_raw_string = 0; + my $in_multiline_macro = 0; + my $in_multiline_string = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Track multi-line comment state + if ($line =~ /\/\*/ && $line !~ /\*\//) { + $in_multiline_comment = 1; + } + if ($in_multiline_comment) { + $in_multiline_comment = 0 if $line =~ /\*\//; + next; + } + + # Skip preprocessor directives (they have their own indentation rules) + next if $line =~ /^\s*#/; + + # Skip blank lines + next if $line =~ /^\s*$/; + + # Skip raw string literals + next if $line =~ /R"[^"]*\(/; + if ($in_raw_string) { + $in_raw_string = 0 if $line =~ /\)"/; + next; + } + if ($line =~ /R"[^"]*\(/ && $line !~ /\)"/) { + $in_raw_string = 1; + next; + } + + # Skip continuation lines (ending with backslash) + if ($in_multiline_macro) { + $in_multiline_macro = ($line =~ /\\$/); + next; + } + if ($line =~ /\\$/) { + $in_multiline_macro = 1; + } + + # Check for tab indentation + if ($line =~ /^\t/) { + report($filepath, $linenum, SEV_ERROR, 'TAB_INDENT', + "Tab indentation detected; use $INDENT_WIDTH spaces per indent level"); + next; + } + + # Check indentation is a multiple of INDENT_WIDTH + if ($line =~ /^( +)\S/) { + my $indent = length($1); + + # Skip lines that are continuation of previous line + # (parameter lists, ternary operators, initializer lists, etc.) + if ($i > 0) { + my $prev_line = $lines_ref->[$i - 1]; + # Continuation patterns: open paren, trailing comma, trailing operator + next if $prev_line =~ /[,({\[]\s*$/; + next if $prev_line =~ /(?:&&|\|\||:|\?)\s*$/; + next if $prev_line =~ /\\$/; + # Initializer list continuation + next if $line =~ /^\s*:/; + next if $line =~ /^\s*,/; + # Alignment with previous line's arguments + next if $indent != 0 && ($indent % $INDENT_WIDTH != 0); + } + + # Only warn on obviously wrong indentation (not alignment) + # This is tricky because alignment is valid, so we're conservative + if ($indent % $INDENT_WIDTH != 0) { + # Check if it could be alignment (e.g., function argument alignment) + # Be lenient: only flag multiples of 2 that aren't multiples of 4 + if ($indent > 0 && $indent % 2 == 0 && $indent % $INDENT_WIDTH != 0) { + # Could be 2-space indent, which is wrong + # But could also be alignment, so make it info-level + report($filepath, $linenum, SEV_INFO, 'INDENT_NOT_MULTIPLE', + "Indentation is $indent spaces (not a multiple of $INDENT_WIDTH). " . + "May be alignment, or incorrect indent width."); + } + } + } + } +} + +# =========================================================================== +# Tab Usage Check +# =========================================================================== + +sub check_tab_usage { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Check for tabs anywhere in the line (not just indentation) + # But skip tab characters in string literals + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; # Remove string literals + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; # Remove char literals + + if ($stripped =~ /\t/ && $line !~ /^\t/) { + report($filepath, $linenum, SEV_WARNING, 'TAB_IN_CODE', + "Tab character found in non-indentation position; use spaces"); + } + } +} + +# =========================================================================== +# Line Length Check +# =========================================================================== + +sub check_line_length { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + my $len = length($line); + + # Skip long include lines - hard to break + next if $line =~ /^\s*#\s*include/; + + # Skip long URLs in comments + next if $line =~ /https?:\/\//; + + # Skip long string literals (difficult to break) + next if $line =~ /^\s*"[^"]*"\s*;?\s*$/; + next if $line =~ /QStringLiteral\s*\(/; + next if $line =~ /QLatin1String\s*\(/; + next if $line =~ /tr\s*\(/; + + if ($len > $MAX_LINE_LENGTH) { + report($filepath, $linenum, SEV_WARNING, 'LINE_TOO_LONG', + "Line is $len characters (maximum: $MAX_LINE_LENGTH)"); + } elsif ($len > $MAX_LINE_LENGTH + 20) { + report($filepath, $linenum, SEV_ERROR, 'LINE_EXCESSIVELY_LONG', + "Line is $len characters (far exceeds maximum: $MAX_LINE_LENGTH)"); + } + } +} + +# =========================================================================== +# Brace Style Check (K&R / 1TBS) +# =========================================================================== + +sub check_brace_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_multiline_comment = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track multi-line comments + if ($line =~ /\/\*/ && $line !~ /\*\//) { + $in_multiline_comment = 1; + } + if ($in_multiline_comment) { + $in_multiline_comment = 0 if $line =~ /\*\//; + next; + } + + next unless exists $changed_ref->{$linenum}; + + # Skip preprocessor + next if $line =~ /^\s*#/; + # Skip string-only lines + next if $line =~ /^\s*"/; + # Skip comments + next if $line =~ /^\s*\/\//; + + # Check: opening brace on its own line after control statement + # MeshMC uses a mixed style - switch statements have brace on next line, + # if/else/for/while have brace on same line (mostly) + # We only flag clearly wrong patterns + + # Check for "} else {" on separate lines (should be on one line) + if ($line =~ /^\s*}\s*$/) { + if ($i + 1 < scalar @$lines_ref) { + my $next_line = $lines_ref->[$i + 1]; + # This is fine in MeshMC style - "} else {" can be split + } + } + + # Opening brace alone on a line after a function-like statement + if ($line =~ /^\s*{\s*$/) { + if ($i > 0) { + my $prev = $lines_ref->[$i - 1]; + + # It's OK for switch statements to have { on next line + next if $prev =~ /\bswitch\s*\(/; + + # It's OK for function definitions to have { on next line + # (constructor initializer list pattern) + next if $prev =~ /^\s*:/; # initializer list + next if $prev =~ /\)\s*$/; # end of function signature + + # It's OK for namespace/class/struct/enum + next if $prev =~ /\b(?:namespace|class|struct|enum)\b/; + + # Control structures should have brace on same line + if ($prev =~ /\b(?:if|else|for|while|do)\b/) { + # This is actually allowed in MeshMC for some cases + # Only flag if the prev line doesn't end with ) + if ($prev =~ /\)\s*$/) { + report($filepath, $linenum, SEV_INFO, 'BRACE_NEXT_LINE_CONTROL', + "Consider placing opening brace on the same line as the control statement (K&R style)"); + } + } + } + } + + # Check for "} else" on same line (this is fine, just check formatting) + if ($line =~ /}\s*else\b/ && $line !~ /}\s+else\b/) { + report($filepath, $linenum, SEV_STYLE, 'BRACE_ELSE_SPACING', + "Missing space between '}' and 'else'"); + } + + # Check: empty else blocks + if ($line =~ /\belse\s*{\s*}\s*$/) { + report($filepath, $linenum, SEV_WARNING, 'EMPTY_ELSE_BLOCK', + "Empty else block"); + } + + # Check: empty if blocks + if ($line =~ /\bif\s*\([^)]*\)\s*{\s*}\s*$/) { + report($filepath, $linenum, SEV_WARNING, 'EMPTY_IF_BLOCK', + "Empty if block"); + } + } +} + +# =========================================================================== +# Naming Convention Check +# =========================================================================== + +sub check_naming_conventions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_class = 0; + my $access_level = ''; + my $in_multiline_comment = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track comments + if ($line =~ /\/\*/ && $line !~ /\*\//) { + $in_multiline_comment = 1; + } + if ($in_multiline_comment) { + $in_multiline_comment = 0 if $line =~ /\*\//; + next; + } + next if $line =~ /^\s*\/\//; + + next unless exists $changed_ref->{$linenum}; + + # Track class/struct context + if ($line =~ /\b(?:class|struct)\s+([A-Za-z_]\w*)\b/) { + my $name = $1; + $in_class = 1; + + # Skip forward declarations + next if $line =~ /;\s*$/; + + # Check class name is PascalCase + unless ($name =~ $RE_PASCAL_CASE) { + report($filepath, $linenum, SEV_WARNING, 'CLASS_NAME_CASE', + "Class/struct name '$name' should be PascalCase"); + } + } + + # Track access level + if ($line =~ /^\s*(public|protected|private)\s*(?::|\s)/) { + $access_level = $1; + } + + # Check member variable naming (m_ prefix) + # Pattern: type m_name; in class context + if ($in_class && $line =~ /^\s+(?:(?:const\s+)?(?:\w+(?:::\w+)*(?:\s*[*&]\s*|\s+)))(m_\w+)\s*[=;]/) { + my $name = $1; + unless ($name =~ $RE_MEMBER_VAR) { + report($filepath, $linenum, SEV_WARNING, 'MEMBER_VAR_NAMING', + "Member variable '$name' should follow m_camelCase convention"); + } + } + + # Check for member variables without m_ prefix in private/protected + if ($in_class && ($access_level eq 'private' || $access_level eq 'protected')) { + # Pattern: "Type varName;" where varName doesn't start with m_ + if ($line =~ /^\s+(?:(?:const\s+)?(?:(?:std::)?(?:shared_ptr|unique_ptr|weak_ptr|vector|map|set|string|optional)\s*<[^>]+>|\w+(?:::\w+)*)\s*[*&]?\s+)([a-z]\w*)\s*[=;]/) { + my $name = $1; + # Skip common false positives + next if $name =~ /^(?:override|const|return|static|virtual|explicit|inline|typename|template)$/; + # Skip function parameters + next if $line =~ /\(/; + + unless ($name =~ /^m_/) { + report($filepath, $linenum, SEV_INFO, 'MEMBER_NO_PREFIX', + "Private/protected member '$name' should use m_ prefix (m_$name)"); + } + } + } + + # Check enum values - PascalCase for enum class + if ($line =~ /\benum\s+class\s+(\w+)/) { + # Check the enum name + my $enum_name = $1; + unless ($enum_name =~ $RE_PASCAL_CASE) { + report($filepath, $linenum, SEV_WARNING, 'ENUM_CLASS_NAME', + "Enum class name '$enum_name' should be PascalCase"); + } + } + + # Check #define macro naming (UPPER_SNAKE_CASE) + if ($line =~ /^\s*#\s*define\s+([A-Za-z_]\w*)/) { + my $name = $1; + # Skip include guards and known Qt macros + next if exists $KNOWN_QT_MACROS{$name}; + next if exists $KNOWN_PROJECT_MACROS{$name}; + # Skip function-like macros that are essentially inline functions + next if $line =~ /#\s*define\s+\w+\s*\(/; + + unless ($name =~ $RE_MACRO_CASE) { + report($filepath, $linenum, SEV_WARNING, 'MACRO_NAME_CASE', + "Macro name '$name' should be UPPER_SNAKE_CASE"); + } + } + + # Check namespace naming + if ($line =~ /\bnamespace\s+([A-Za-z_]\w*)/) { + my $name = $1; + # MeshMC uses PascalCase for namespaces + unless ($name =~ $RE_PASCAL_CASE || $name =~ $RE_SNAKE_CASE) { + report($filepath, $linenum, SEV_INFO, 'NAMESPACE_NAMING', + "Namespace '$name' should use PascalCase (project convention)"); + } + } + + # End of class + if ($line =~ /^};/) { + $in_class = 0; + $access_level = ''; + } + } +} + +# =========================================================================== +# Pointer/Reference Alignment Check +# =========================================================================== + +sub check_pointer_alignment { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments, preprocessor, string literals + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*#/; + next if $line =~ /^\s*\*/; # Inside block comment + + # Remove string literals for analysis + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; + + # Check for pointer attached to type name instead of variable name + # Pattern: "Type* var" should be "Type *var" (MeshMC convention) + # But this is complex - MeshMC actually uses "Type *var" style + if ($stripped =~ /\b(\w+)\*\s+(\w+)/ && $stripped !~ /\*\//) { + my $type = $1; + my $var = $2; + # Skip common false positives + next if $type =~ /^(?:const|return|void|char|int|long|short|unsigned|signed|float|double|bool|auto)$/; + next if $stripped =~ /operator\s*\*/; + next if $stripped =~ /\bdelete\b/; + next if $stripped =~ /template/; + + # In MeshMC, the preferred style is "Type *var" (space before *) + report($filepath, $linenum, SEV_STYLE, 'POINTER_ALIGNMENT', + "Pointer '*' should be adjacent to variable name, not type " . + "(use '$type *$var' instead of '$type* $var')"); + } + + # Check reference alignment similarly + if ($stripped =~ /\b(\w+)&\s+(\w+)/ && $stripped !~ /&&/) { + my $type = $1; + my $var = $2; + next if $type =~ /^(?:const|return|void|char|int|long|short|unsigned|signed|float|double|bool|auto)$/; + next if $stripped =~ /operator\s*&/; + + # Same convention for references + report($filepath, $linenum, SEV_STYLE, 'REFERENCE_ALIGNMENT', + "Reference '&' should be adjacent to variable name, not type " . + "(use '$type &$var' instead of '$type& $var')"); + } + } +} + +# =========================================================================== +# Spacing Conventions Check +# =========================================================================== + +sub check_spacing_conventions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments and preprocessor + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + next if $line =~ /^\s*#/; + + # Remove string literals + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"/""/g; + $stripped =~ s/'(?:[^'\\]|\\.)*'/''/g; + + # Check space after keywords + foreach my $keyword (qw(if for while switch catch do)) { + if ($stripped =~ /\b$keyword\(/) { + report($filepath, $linenum, SEV_STYLE, 'KEYWORD_SPACE', + "Missing space between '$keyword' and '(' - use '$keyword ('"); + } + } + + # Check no space before semicolons + if ($stripped =~ /\s;/ && $stripped !~ /for\s*\(/) { + report($filepath, $linenum, SEV_STYLE, 'SPACE_BEFORE_SEMICOLON', + "Unexpected space before semicolon"); + } + + # Check space after commas + if ($stripped =~ /,\S/ && $stripped !~ /,\s*$/) { + # Skip template arguments and nested parentheses + next if $stripped =~ /[<>]/; + report($filepath, $linenum, SEV_STYLE, 'SPACE_AFTER_COMMA', + "Missing space after comma"); + } + + # Check spaces around binary operators (=, ==, !=, <=, >=, &&, ||, +, -, etc.) + # Be careful not to flag unary operators, pointers, references, etc. + if ($stripped =~ /\w=[^=]/ && $stripped !~ /[!<>=]=/ && $stripped !~ /operator/) { + # Assignment without space + if ($stripped =~ /(\w)=([^\s=])/ && $stripped !~ /[<>]/) { + # Very common false positive - skip template/default args + # Only flag obvious cases + } + } + + # Check for double spaces (excluding indentation and alignment) + if ($stripped =~ /\S \S/ && $stripped !~ /\/\//) { + # Skip alignment patterns + next if $stripped =~ /^\s+\w+\s{2,}\w/; # Aligned declarations + report($filepath, $linenum, SEV_INFO, 'DOUBLE_SPACE', + "Multiple consecutive spaces in code (not indentation)"); + } + + # Check space before opening brace + if ($stripped =~ /\S\{/ && $stripped !~ /\$\{/ && $stripped !~ /\\\{/) { + # Skip lambda captures, initializer lists + next if $stripped =~ /\]\s*{/; + next if $stripped =~ /=\s*{/; + next if $stripped =~ /return\s*{/; + next if $stripped =~ /^\s*{/; + next if $stripped =~ /R"\w*\(/; # raw string + + report($filepath, $linenum, SEV_STYLE, 'SPACE_BEFORE_BRACE', + "Missing space before opening brace '{'"); + } + } +} + +# =========================================================================== +# Qt Convention Checks +# =========================================================================== + +sub check_qt_conventions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $has_qobject = 0; + my $in_class = 0; + my $class_name = ''; + my $has_virtual_destructor = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track class definition + if ($line =~ /\bclass\s+(\w+)\b.*:\s*public\s+QObject\b/) { + $in_class = 1; + $class_name = $1; + $has_qobject = 0; + $has_virtual_destructor = 0; + } + + # Check for Q_OBJECT macro in QObject-derived classes + if ($in_class && $line =~ /\bQ_OBJECT\b/) { + $has_qobject = 1; + } + + if ($in_class && $line =~ /virtual\s+~/) { + $has_virtual_destructor = 1; + } + + # End of class + if ($in_class && $line =~ /^};/) { + if (!$has_qobject && $class_name) { + report($filepath, $linenum, SEV_ERROR, 'MISSING_Q_OBJECT', + "Class '$class_name' derived from QObject missing Q_OBJECT macro"); + } + $in_class = 0; + $class_name = ''; + } + + next unless exists $changed_ref->{$linenum}; + + # Check for old-style Qt connect() with SIGNAL/SLOT macros + if ($line =~ /\bconnect\s*\(.*\bSIGNAL\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'OLD_QT_CONNECT', + "Old-style SIGNAL/SLOT connect() syntax; prefer new-style pointer-to-member syntax"); + } + + # Check for Q_EMIT vs emit + if ($line =~ /\bemit\s+\w+/ && $line !~ /\bQ_EMIT\b/) { + # In MeshMC, both 'emit' and direct signal calls are used + # Just note it as style + report($filepath, $linenum, SEV_INFO, 'EMIT_KEYWORD', + "Consider using Q_EMIT instead of 'emit' for clarity"); + } + + # Check for QObject::tr() in non-QObject contexts + if ($line =~ /QObject::tr\s*\(/) { + report($filepath, $linenum, SEV_INFO, 'QOBJECT_TR', + "Using QObject::tr() directly; consider if the class should use Q_OBJECT and tr()"); + } + + # Check signals/slots sections formatting + if ($line =~ /^(signals|public\s+slots|private\s+slots|protected\s+slots)\s*:/) { + # OK - proper format + } elsif ($line =~ /^(Q_SIGNALS|Q_SLOTS)\s*:/) { + report($filepath, $linenum, SEV_STYLE, 'QT_SIGNAL_SLOT_SYNTAX', + "Prefer 'signals:' and 'slots:' over Q_SIGNALS/Q_SLOTS macros"); + } + } +} + +# =========================================================================== +# Include Style Check +# =========================================================================== + +sub check_include_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + if ($line =~ /^\s*#\s*include\s+(.+)/) { + my $include = $1; + + # Check for correct include syntax + unless ($include =~ /^[<"]/) { + report($filepath, $linenum, SEV_ERROR, 'INCLUDE_SYNTAX', + "Invalid include syntax: should be #include <file> or #include \"file\""); + next; + } + + # Check for C headers that have C++ equivalents + my %c_to_cpp = ( + '<stdio.h>' => '<cstdio>', + '<stdlib.h>' => '<cstdlib>', + '<string.h>' => '<cstring>', + '<math.h>' => '<cmath>', + '<ctype.h>' => '<cctype>', + '<assert.h>' => '<cassert>', + '<stddef.h>' => '<cstddef>', + '<stdint.h>' => '<cstdint>', + '<time.h>' => '<ctime>', + '<limits.h>' => '<climits>', + '<float.h>' => '<cfloat>', + '<errno.h>' => '<cerrno>', + '<signal.h>' => '<csignal>', + '<setjmp.h>' => '<csetjmp>', + '<locale.h>' => '<clocale>', + ); + + foreach my $c_header (keys %c_to_cpp) { + if ($include =~ /^\Q$c_header\E/) { + report($filepath, $linenum, SEV_WARNING, 'C_HEADER_IN_CPP', + "Use C++ header $c_to_cpp{$c_header} instead of C header $c_header"); + } + } + + # Check for absolute paths in includes + if ($include =~ /^"\//) { + report($filepath, $linenum, SEV_ERROR, 'ABSOLUTE_INCLUDE_PATH', + "Absolute path in #include directive; use relative paths"); + } + + # Check for backward slashes in include paths + if ($include =~ /\\/) { + report($filepath, $linenum, SEV_ERROR, 'BACKSLASH_IN_INCLUDE', + "Backslash in #include path; use forward slashes"); + } + } + } +} + +# =========================================================================== +# Comment Style Check +# =========================================================================== + +sub check_comment_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_multiline_comment = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track multiline comments + if ($line =~ /\/\*/ && $line !~ /\*\//) { + $in_multiline_comment = 1; + } + if ($in_multiline_comment) { + $in_multiline_comment = 0 if $line =~ /\*\//; + + # Check alignment of multi-line comment body + if ($line =~ /^(\s*)\*/ && $line !~ /^\s*\/\*/) { + # Comment continuation - * should be aligned + } + next; + } + + next unless exists $changed_ref->{$linenum}; + + # Check for C-style comments on single lines (should use //) + if ($line =~ /\/\*(.+?)\*\/\s*$/ && $line !~ /^\s*\/\*/) { + my $comment = $1; + # Skip if it's a license header or Doxygen + next if $comment =~ /SPDX|copyright|doxygen|\@|\\brief/i; + # Single-line comments should prefer // + report($filepath, $linenum, SEV_INFO, 'COMMENT_STYLE', + "Consider using // for single-line comments instead of /* */"); + } + + # Check for commented-out code (rough heuristic) + if ($line =~ /^\s*\/\/\s*(if|for|while|return|switch|class|struct|void|int|bool|auto|QString)\b/) { + report($filepath, $linenum, SEV_WARNING, 'COMMENTED_OUT_CODE', + "Possible commented-out code detected; remove dead code"); + } + + # Check for TODO/FIXME format + if ($line =~ /\/\/\s*(TODO|FIXME|HACK|XXX|BUG)\b/) { + # Proper format: // TODO: Description or // TODO(author): Description + unless ($line =~ /\/\/\s*(?:TODO|FIXME|HACK|XXX|BUG)\s*(?:\(\w+\)\s*)?:\s*\S/) { + report($filepath, $linenum, SEV_INFO, 'TODO_FORMAT', + "TODO/FIXME should follow format: // TODO: Description or // TODO(author): Description"); + } + } + } +} + +# =========================================================================== +# Control Flow Style Check +# =========================================================================== + +sub check_control_flow_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for single-line if/else without braces + # MeshMC allows single-line if without braces for simple returns + # But multi-line if without braces is dangerous + if ($line =~ /\bif\s*\(.*\)\s*$/ && $line !~ /{\s*$/) { + if ($i + 1 < scalar @$lines_ref) { + my $next = $lines_ref->[$i + 1]; + # Next line is not an opening brace + if ($next !~ /^\s*{/) { + # Check if it's a simple single-statement if + if ($i + 2 < scalar @$lines_ref) { + my $after = $lines_ref->[$i + 2]; + # If the line after the body has more code, it's OK (single statement) + # But warn anyway for consistency + report($filepath, $linenum, SEV_INFO, 'IF_WITHOUT_BRACES', + "Consider using braces for 'if' statement body for consistency"); + } + } + } + } + + # Check for goto usage + if ($line =~ /\bgoto\s+\w+/) { + report($filepath, $linenum, SEV_WARNING, 'GOTO_STATEMENT', + "Use of 'goto' statement; consider restructuring with loops/functions"); + } + + # Check for deeply nested ternary + if ($line =~ /\?.*\?.*:.*:/) { + report($filepath, $linenum, SEV_WARNING, 'NESTED_TERNARY', + "Nested ternary operator; consider using if/else for readability"); + } + + # Check for Yoda conditions (constant == variable) + if ($line =~ /\b(?:nullptr|NULL|true|false|\d+)\s*==\s*\w/) { + report($filepath, $linenum, SEV_STYLE, 'YODA_CONDITION', + "Yoda condition detected; prefer 'variable == constant' style"); + } + + # Check assignment in condition + if ($line =~ /\bif\s*\([^)]*[^!=<>]=[^=][^)]*\)/) { + # This could be intentional (e.g., if (auto x = ...)) + # Only flag clear assignment cases + if ($line !~ /\bauto\b/ && $line !~ /\bif\s*\(\s*\w+\s*=\s*\w+\.\w+\(/) { + report($filepath, $linenum, SEV_WARNING, 'ASSIGNMENT_IN_CONDITION', + "Possible assignment in condition; did you mean '=='?"); + } + } + } +} + +# =========================================================================== +# String Usage Check +# =========================================================================== + +sub check_string_usage { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for std::string usage (project uses QString) + if ($line =~ /\bstd::string\b/) { + report($filepath, $linenum, SEV_WARNING, 'STD_STRING_USAGE', + "Use QString instead of std::string (project convention)"); + } + + # Check for std::cout/std::cerr (project uses Qt logging) + if ($line =~ /\bstd::(?:cout|cerr|clog)\b/) { + report($filepath, $linenum, SEV_WARNING, 'STD_IOSTREAM', + "Use qDebug()/qWarning()/qCritical() instead of std::cout/cerr (project convention)"); + } + + # Check for printf/fprintf usage + if ($line =~ /\b(?:printf|fprintf|puts)\s*\(/ && $line !~ /::snprintf/) { + report($filepath, $linenum, SEV_WARNING, 'PRINTF_USAGE', + "Use qDebug()/qWarning() or QTextStream instead of printf/fprintf"); + } + + # Check for empty string comparisons + if ($line =~ /==\s*""/ || $line =~ /""\s*==/) { + report($filepath, $linenum, SEV_STYLE, 'EMPTY_STRING_COMPARE', + "Use QString::isEmpty() instead of comparing with empty string \"\""); + } + + # Check for string concatenation in loops (potential performance issue) + # This is hard to detect accurately, so we just look for obvious patterns + if ($line =~ /\+=\s*"/ || $line =~ /\+=\s*QString/) { + # Only flag if we're inside a for/while loop (check context) + for (my $j = $i - 1; $j >= 0 && $j >= $i - 20; $j--) { + if ($lines_ref->[$j] =~ /\b(?:for|while)\s*\(/) { + report($filepath, $linenum, SEV_INFO, 'STRING_CONCAT_LOOP', + "String concatenation in loop; consider using QStringList::join() or QStringBuilder"); + last; + } + last if $lines_ref->[$j] =~ /^[{}]\s*$/; + } + } + } +} + +# =========================================================================== +# Memory Management Pattern Check +# =========================================================================== + +sub check_memory_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for naked new without smart pointer + if ($line =~ /\bnew\s+\w+/ && $line !~ /(?:shared_ptr|unique_ptr|make_shared|make_unique|reset|QObject|QWidget|QLayout|Q\w+)/) { + # Skip Qt object creation (parent takes ownership) + next if $line =~ /new\s+\w+\s*\([^)]*(?:this|parent)\s*[,)]/; + # Skip placement new + next if $line =~ /new\s*\(/; + # Skip operator new + next if $line =~ /operator\s+new/; + + report($filepath, $linenum, SEV_INFO, 'RAW_NEW', + "Use std::make_shared/std::make_unique instead of raw 'new' when possible"); + } + + # Check for delete usage (should use smart pointers) + if ($line =~ /\bdelete\b/ && $line !~ /\bQ_DISABLE\b/ && $line !~ /=\s*delete/) { + report($filepath, $linenum, SEV_INFO, 'RAW_DELETE', + "Raw 'delete' detected; prefer smart pointers for automatic memory management"); + } + + # Check for C-style malloc/calloc/free + if ($line =~ /\b(?:malloc|calloc|realloc|free)\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'C_MEMORY_FUNCTIONS', + "C-style memory functions (malloc/free) detected; use C++ alternatives (new/delete or smart pointers)"); + } + + # Check for proper shared_ptr usage + if ($line =~ /std::shared_ptr<\w+>\s*\(\s*new\b/) { + report($filepath, $linenum, SEV_WARNING, 'SHARED_PTR_NEW', + "Use std::make_shared<T>() instead of std::shared_ptr<T>(new T())"); + } + + # Check for unique_ptr with new + if ($line =~ /std::unique_ptr<\w+>\s*\(\s*new\b/) { + report($filepath, $linenum, SEV_WARNING, 'UNIQUE_PTR_NEW', + "Use std::make_unique<T>() instead of std::unique_ptr<T>(new T())"); + } + } +} + +# =========================================================================== +# Const Correctness Check +# =========================================================================== + +sub check_const_correctness { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for non-const reference parameters to complex types + # that appear to be input-only (no modification) + if ($line =~ /(?:QString|QStringList|QList|QVector|QMap|QHash|QSet|QByteArray|std::(?:string|vector|map|set))\s*&\s*(\w+)/) { + my $param = $1; + # If it's a function parameter (has type before &) + if ($line =~ /\(.*$/ || $line =~ /,\s*$/) { + # Check if const is missing + unless ($line =~ /const\s+(?:QString|QStringList|QList|QVector|QMap|QHash|QSet|QByteArray|std::(?:string|vector|map|set))\s*&/) { + # This could be intentional (output parameter), so make it info level + report($filepath, $linenum, SEV_INFO, 'CONST_REF_PARAM', + "Consider using const reference for parameter '$param' if not modified"); + } + } + } + } +} + +# =========================================================================== +# Enum Style Check +# =========================================================================== + +sub check_enum_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Check for plain enum (prefer enum class) + if ($line =~ /\benum\s+(\w+)\s*{/ && $line !~ /\benum\s+class\b/) { + my $name = $1; + report($filepath, $linenum, SEV_INFO, 'PREFER_ENUM_CLASS', + "Consider using 'enum class $name' instead of plain 'enum $name' for type safety"); + } + + # Check for anonymous enum + if ($line =~ /\benum\s*{/ && $line !~ /\benum\s+\w/) { + report($filepath, $linenum, SEV_INFO, 'ANONYMOUS_ENUM', + "Anonymous enum detected; consider naming it or using 'constexpr'"); + } + } +} + +# =========================================================================== +# Constructor Pattern Check +# =========================================================================== + +sub check_constructor_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for single-parameter constructors without explicit + if ($line =~ /^\s*(\w+)\s*\(\s*(?:const\s+)?(?:\w+(?:::\w+)*)(?:\s+[&*]?\s*\w+|\s*[&*]+\s*\w+)\s*\)\s*[;{]/ && + $line !~ /\bexplicit\b/ && $line !~ /\bvirtual\b/ && $line !~ /\boverride\b/) { + my $class = $1; + # Skip destructors, operators + next if $class =~ /^~/; + next if $class =~ /^operator/; + # Skip copy/move constructors + next if $line =~ /\b$class\s*\(\s*(?:const\s+)?$class\s*[&]/; + + report($filepath, $linenum, SEV_WARNING, 'IMPLICIT_CONSTRUCTOR', + "Single-parameter constructor should be marked 'explicit' to prevent implicit conversions"); + } + + # Check for assignment in constructor body instead of initializer list + # (When the previous line ends with { and current line is "m_var = ...") + if ($line =~ /^\s+m_\w+\s*=\s*\w/ && $i > 0) { + my $prev = $lines_ref->[$i - 1]; + if ($prev =~ /^{$/ || $prev =~ /^\s*{$/) { + # Check if a few lines up we have a constructor signature + for (my $j = $i - 2; $j >= 0 && $j >= $i - 5; $j--) { + if ($lines_ref->[$j] =~ /\w+::\w+\s*\(/ && $lines_ref->[$j] !~ /\bif\b|\bfor\b|\bwhile\b/) { + report($filepath, $linenum, SEV_INFO, 'USE_INIT_LIST', + "Consider using initializer list instead of assignment in constructor body"); + last; + } + } + } + } + } +} + +# =========================================================================== +# Function Length Check +# =========================================================================== + +sub check_function_length { + my ($filepath, $lines_ref) = @_; + + my $brace_depth = 0; + my $function_start = -1; + my $function_name = ''; + my $in_function = 0; + my $brace_counted_line = -1; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + # Skip preprocessor directives + next if $line =~ /^\s*#/; + + # Detect function definition start + if (!$in_function && $line =~ /^(?:\w[\w:*&<> ,]*\s+)?(\w+(?:::\w+)?)\s*\([^;]*$/) { + my $name = $1; + if ($line =~ /{/) { + $function_start = $i; + $function_name = $name; + $in_function = 1; + $brace_depth = count_braces($line); + $brace_counted_line = $i; + } elsif ($i + 1 < scalar @$lines_ref && $lines_ref->[$i + 1] =~ /^\s*{/) { + $function_start = $i; + $function_name = $name; + } + } + + # Handle deferred function start (opening brace on next line) + if ($function_start >= 0 && !$in_function && $line =~ /{/) { + $in_function = 1; + $brace_depth = count_braces($line); + $brace_counted_line = $i; + } + + # Count braces for subsequent lines (skip already-counted start line) + if ($in_function && $i != $brace_counted_line) { + $brace_depth += count_braces($line); + } + + if ($in_function && $brace_depth <= 0 && $line =~ /}/) { + my $length = $i - $function_start + 1; + if ($length > $MAX_FUNCTION_LENGTH) { + report($filepath, $function_start + 1, SEV_WARNING, 'FUNCTION_TOO_LONG', + "Function '$function_name' is $length lines (recommended maximum: $MAX_FUNCTION_LENGTH). Consider refactoring."); + } + $in_function = 0; + $function_start = -1; + $function_name = ''; + $brace_depth = 0; + $brace_counted_line = -1; + } + } +} + +sub count_braces { + my ($line) = @_; + # Remove string literals and comments + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; + $stripped =~ s/\/\/.*$//; + + my $open = () = $stripped =~ /{/g; + my $close = () = $stripped =~ /}/g; + return $open - $close; +} + +# =========================================================================== +# Nesting Depth Check +# =========================================================================== + +sub check_nesting_depth { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $depth = 0; + my $max_depth = 0; + my $max_depth_line = 0; + my $in_function = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Remove strings and comments + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; + $stripped =~ s/\/\/.*$//; + + my $opens = () = $stripped =~ /{/g; + my $closes = () = $stripped =~ /}/g; + + $depth += $opens; + + if ($depth > $MAX_NESTING_DEPTH && exists $changed_ref->{$linenum}) { + if ($depth > $max_depth) { + $max_depth = $depth; + $max_depth_line = $linenum; + } + } + + $depth -= $closes; + $depth = 0 if $depth < 0; + } + + if ($max_depth > $MAX_NESTING_DEPTH) { + report($filepath, $max_depth_line, SEV_WARNING, 'DEEP_NESTING', + "Nesting depth of $max_depth exceeds maximum of $MAX_NESTING_DEPTH. Consider refactoring."); + } +} + +# =========================================================================== +# Deprecated Pattern Check +# =========================================================================== + +sub check_deprecated_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for NULL instead of nullptr + if ($line =~ /\bNULL\b/ && $line !~ /#\s*define/) { + report($filepath, $linenum, SEV_WARNING, 'USE_NULLPTR', + "Use 'nullptr' instead of 'NULL' (C++11)"); + } + + # Check for C-style casts + if ($line =~ /\(\s*(?:int|long|short|char|float|double|unsigned|signed|bool|void)\s*[*]*\s*\)\s*\w/) { + report($filepath, $linenum, SEV_WARNING, 'C_STYLE_CAST', + "C-style cast detected; use static_cast<>, dynamic_cast<>, reinterpret_cast<>, or const_cast<>"); + } + + # Check for typedef instead of using (prefer using) + if ($line =~ /\btypedef\b/) { + report($filepath, $linenum, SEV_STYLE, 'PREFER_USING', + "Consider using 'using' alias instead of 'typedef' (modern C++ style)"); + } + + # Check for deprecated QList patterns + if ($line =~ /\bQList<(?:int|double|float|bool|char|qint\d+|quint\d+)>/) { + report($filepath, $linenum, SEV_INFO, 'QLIST_VALUE_TYPE', + "QList of basic types; consider QVector for better performance in Qt5"); + } + + # Check for deprecated Qt functions + if ($line =~ /\b(?:qSort|qStableSort|qLowerBound|qUpperBound|qBinaryFind)\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'DEPRECATED_QT_ALGORITHM', + "Deprecated Qt algorithm; use std:: equivalents (std::sort, std::stable_sort, etc.)"); + } + + # Check for register keyword + if ($line =~ /\bregister\b/ && $line !~ /^\s*\/\//) { + report($filepath, $linenum, SEV_WARNING, 'REGISTER_KEYWORD', + "'register' keyword is deprecated in C++17 and removed in C++20"); + } + + # Check for auto_ptr (deprecated) + if ($line =~ /\bstd::auto_ptr\b/) { + report($filepath, $linenum, SEV_ERROR, 'AUTO_PTR', + "std::auto_ptr is removed in C++17; use std::unique_ptr instead"); + } + + # Check for bind1st/bind2nd (deprecated) + if ($line =~ /\bstd::(?:bind1st|bind2nd)\b/) { + report($filepath, $linenum, SEV_ERROR, 'DEPRECATED_BIND', + "std::bind1st/bind2nd are removed in C++17; use std::bind or lambdas"); + } + } +} + +# =========================================================================== +# Security Pattern Check +# =========================================================================== + +sub check_security_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for dangerous functions + if ($line =~ /\b(?:gets)\s*\(/) { + report($filepath, $linenum, SEV_ERROR, 'DANGEROUS_FUNCTION', + "Use of 'gets' is extremely dangerous (buffer overflow); use fgets or std::getline"); + } + + if ($line =~ /\bsprintf\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'SPRINTF_USAGE', + "Use snprintf instead of sprintf to prevent buffer overflow"); + } + + if ($line =~ /\bstrcpy\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'STRCPY_USAGE', + "Use strncpy or std::string instead of strcpy to prevent buffer overflow"); + } + + if ($line =~ /\bstrcat\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'STRCAT_USAGE', + "Use strncat or std::string instead of strcat to prevent buffer overflow"); + } + + # Check for system() calls + if ($line =~ /\bsystem\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'SYSTEM_CALL', + "Use of system() is a security risk; use QProcess for safer process execution"); + } + + # Check for potential format string vulnerabilities + if ($line =~ /\b(?:printf|fprintf|sprintf|snprintf)\s*\(\s*(?:\w+)\s*\)/) { + report($filepath, $linenum, SEV_WARNING, 'FORMAT_STRING', + "Potential format string vulnerability; always use a literal format string"); + } + + # Check for hardcoded credentials patterns + if ($line =~ /(?:password|passwd|secret|api_?key|token)\s*=\s*"[^"]+"/i) { + # Skip test files and examples + next if $filepath =~ /_test\.cpp$/; + report($filepath, $linenum, SEV_ERROR, 'HARDCODED_CREDENTIALS', + "Possible hardcoded credentials detected; use configuration files or environment variables"); + } + + # Check for use of rand() (use modern random) + if ($line =~ /\brand\s*\(\s*\)/ && $line !~ /\bstd::/) { + report($filepath, $linenum, SEV_INFO, 'C_RAND', + "Use <random> header (std::mt19937, std::uniform_int_distribution) instead of rand()"); + } + } +} + +# =========================================================================== +# Test Convention Check +# =========================================================================== + +sub check_test_conventions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + # Only check test files + return unless $filepath =~ /_test\.cpp$/; + + my $has_qtest_include = 0; + my $has_qtest_main = 0; + my $has_moc_include = 0; + my $has_qobject = 0; + my $test_class_name = ''; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + if ($line =~ /#\s*include\s*<QTest>/) { + $has_qtest_include = 1; + } + + if ($line =~ /QTEST_(?:GUILESS_)?MAIN\s*\((\w+)\)/) { + $has_qtest_main = 1; + my $main_class = $1; + if ($test_class_name && $main_class ne $test_class_name) { + report($filepath, $linenum, SEV_ERROR, 'TEST_MAIN_MISMATCH', + "QTEST_MAIN class '$main_class' doesn't match test class '$test_class_name'"); + } + } + + if ($line =~ /#\s*include\s+"(\w+)_test\.moc"/) { + $has_moc_include = 1; + } + + if ($line =~ /\bclass\s+(\w+Test)\s*:/) { + $test_class_name = $1; + } + + if ($line =~ /Q_OBJECT/) { + $has_qobject = 1; + } + + # Check test method naming + if ($line =~ /^\s*void\s+(test\w*)\s*\(/) { + my $method = $1; + # MeshMC test methods use test_PascalCase pattern + unless ($method =~ /^test_[A-Z]\w*$/ || $method =~ /^test_\w+_data$/) { + report($filepath, $linenum, SEV_STYLE, 'TEST_METHOD_NAMING', + "Test method '$method' should follow 'test_PascalCase' naming convention"); + } + } + + # Check for QVERIFY(true) or QVERIFY(false) - likely placeholder + if ($line =~ /QVERIFY\s*\(\s*(?:true|false)\s*\)/) { + report($filepath, $linenum, SEV_WARNING, 'TRIVIAL_ASSERTION', + "Trivial assertion QVERIFY(true/false) - likely a placeholder"); + } + } + + if (!$has_qtest_include) { + report($filepath, 1, SEV_ERROR, 'MISSING_QTEST_INCLUDE', + "Test file missing #include <QTest>"); + } + + if (!$has_qtest_main) { + report($filepath, scalar @$lines_ref, SEV_ERROR, 'MISSING_QTEST_MAIN', + "Test file missing QTEST_GUILESS_MAIN() or QTEST_MAIN() macro"); + } + + if (!$has_moc_include && $has_qobject) { + report($filepath, scalar @$lines_ref, SEV_ERROR, 'MISSING_MOC_INCLUDE', + "Test file with Q_OBJECT missing #include \"*_test.moc\" at end of file"); + } +} + +# =========================================================================== +# Exception Pattern Check +# =========================================================================== + +sub check_exception_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for throwing by pointer (throw new Exception) + if ($line =~ /\bthrow\s+new\b/) { + report($filepath, $linenum, SEV_ERROR, 'THROW_BY_POINTER', + "Throw by value, not by pointer (throw Exception(...), not throw new Exception(...))"); + } + + # Check for catching by value (should catch by reference) + if ($line =~ /\bcatch\s*\(\s*(?!const\s)(\w[\w:]*)\s+\w+\s*\)/) { + my $type = $1; + next if $type eq '...'; # catch-all + report($filepath, $linenum, SEV_WARNING, 'CATCH_BY_VALUE', + "Catch exceptions by const reference: catch(const $type& e)"); + } + + # Check for catching by non-const reference + if ($line =~ /\bcatch\s*\(\s*(\w[\w:]*)\s*&\s*\w+\s*\)/ && $line !~ /\bconst\b/) { + report($filepath, $linenum, SEV_STYLE, 'CATCH_NON_CONST_REF', + "Catch exceptions by const reference: catch(const ... &)"); + } + + # Check for empty catch blocks + if ($line =~ /\bcatch\b/) { + if ($i + 1 < scalar @$lines_ref) { + my $next = $lines_ref->[$i + 1]; + if ($next =~ /^\s*{\s*}\s*$/ || ($line =~ /{\s*$/ && $i + 1 < scalar @$lines_ref && $lines_ref->[$i + 1] =~ /^\s*}\s*$/)) { + report($filepath, $linenum, SEV_WARNING, 'EMPTY_CATCH', + "Empty catch block; at minimum, add a comment explaining why it's empty"); + } + } + } + + # Check for catch(...) without re-throw + if ($line =~ /\bcatch\s*\(\s*\.\.\.\s*\)/) { + # Check if body contains throw + my $has_throw = 0; + for (my $j = $i + 1; $j < scalar @$lines_ref && $j < $i + 20; $j++) { + if ($lines_ref->[$j] =~ /\bthrow\b/) { + $has_throw = 1; + last; + } + last if $lines_ref->[$j] =~ /^}/; + } + if (!$has_throw) { + report($filepath, $linenum, SEV_WARNING, 'CATCH_ALL_NO_RETHROW', + "catch(...) without re-throw may swallow important exceptions"); + } + } + } +} + +# =========================================================================== +# Virtual Destructor Check +# =========================================================================== + +sub check_virtual_destructor { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_class = 0; + my $class_name = ''; + my $has_virtual_method = 0; + my $has_virtual_destructor = 0; + my $class_start_line = 0; + my $brace_depth = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + # Detect class start + if ($line =~ /\bclass\s+(\w+)\b/ && $line !~ /;\s*$/) { + $in_class = 1; + $class_name = $1; + $has_virtual_method = 0; + $has_virtual_destructor = 0; + $class_start_line = $i + 1; + $brace_depth = 0; + } + + next unless $in_class; + + # Track braces + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + my $opens = () = $stripped =~ /{/g; + my $closes = () = $stripped =~ /}/g; + $brace_depth += $opens - $closes; + + if ($line =~ /\bvirtual\b/ && $line !~ /~/) { + $has_virtual_method = 1; + } + + if ($line =~ /virtual\s+~/ || $line =~ /~\w+\s*\(\s*\)\s*(?:=\s*default|override)/) { + $has_virtual_destructor = 1; + } + + # End of class + if ($brace_depth <= 0 && $line =~ /}/) { + if ($has_virtual_method && !$has_virtual_destructor) { + report($filepath, $class_start_line, SEV_WARNING, 'MISSING_VIRTUAL_DESTRUCTOR', + "Class '$class_name' has virtual methods but no virtual destructor"); + } + $in_class = 0; + } + } +} + +# =========================================================================== +# Override Keyword Check +# =========================================================================== + +sub check_override_keyword { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_derived_class = 0; + my $class_name = ''; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track derived class context + if ($line =~ /\bclass\s+(\w+)\s*:\s*(?:public|protected|private)/) { + $in_derived_class = 1; + $class_name = $1; + } + + if ($in_derived_class && $line =~ /^};/) { + $in_derived_class = 0; + } + + next unless $in_derived_class; + next unless exists $changed_ref->{$linenum}; + + # Check for virtual without override in derived class + if ($line =~ /\bvirtual\b.*\)\s*(?:const\s*)?;/ && $line !~ /\boverride\b/ && $line !~ /=\s*0/) { + report($filepath, $linenum, SEV_WARNING, 'MISSING_OVERRIDE', + "Virtual method in derived class should use 'override' keyword"); + } + + # Check for both virtual and override (redundant) + if ($line =~ /\bvirtual\b.*\boverride\b/) { + report($filepath, $linenum, SEV_STYLE, 'VIRTUAL_AND_OVERRIDE', + "'virtual' is redundant when 'override' is used"); + } + } +} + +# =========================================================================== +# auto Usage Check +# =========================================================================== + +sub check_auto_usage { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for auto& without const for non-modifying iteration + # This is hard to detect accurately, so be conservative + if ($line =~ /\bfor\s*\(\s*auto\s+&\s+\w+\s*:/ && $line !~ /\bconst\b/) { + report($filepath, $linenum, SEV_INFO, 'AUTO_REF_ITERATION', + "Consider 'const auto&' for range-based for loop if elements aren't modified"); + } + + # Check for auto in function return type (hard to read) + if ($line =~ /^\s*(?:static\s+)?(?:inline\s+)?auto\s+\w+\s*\(/) { + # Trailing return type is OK + unless ($line =~ /->/) { + report($filepath, $linenum, SEV_INFO, 'AUTO_RETURN_TYPE', + "Auto return type without trailing return type can reduce readability"); + } + } + } +} + +# =========================================================================== +# Lambda Style Check +# =========================================================================== + +sub check_lambda_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for [=] capture (captures everything by value - usually too broad) + if ($line =~ /\[\s*=\s*\]/) { + report($filepath, $linenum, SEV_INFO, 'LAMBDA_CAPTURE_ALL_VALUE', + "Lambda captures everything by value [=]; consider explicit captures"); + } + + # Check for [&] capture (captures everything by reference) + # This is used in MeshMC but worth noting for awareness + if ($line =~ /\[\s*&\s*\]/ && $line !~ /\[\s*&\s*\]\s*\(\s*\)\s*{/) { + # Only flag in certain contexts (e.g., stored lambdas vs inline) + if ($line =~ /=\s*\[\s*&\s*\]/) { + report($filepath, $linenum, SEV_INFO, 'LAMBDA_CAPTURE_ALL_REF', + "Lambda captures everything by reference [&]; be careful with object lifetimes"); + } + } + + # Check for overly long lambda bodies (should be extracted to a function) + if ($line =~ /\[.*\]\s*\(/) { + # Count lines until matching closing brace + my $lambda_depth = 0; + my $lambda_lines = 0; + my $started = 0; + + for (my $j = $i; $j < scalar @$lines_ref && $j < $i + 100; $j++) { + my $l = $lines_ref->[$j]; + my $stripped = $l; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + + if ($stripped =~ /{/) { + $lambda_depth += () = $stripped =~ /{/g; + $started = 1; + } + if ($stripped =~ /}/) { + $lambda_depth -= () = $stripped =~ /}/g; + } + + $lambda_lines++ if $started; + + if ($started && $lambda_depth <= 0) { + if ($lambda_lines > 30) { + report($filepath, $linenum, SEV_INFO, 'LONG_LAMBDA', + "Lambda body is $lambda_lines lines; consider extracting to a named function"); + } + last; + } + } + } + } +} + +# =========================================================================== +# Switch/Case Style Check +# =========================================================================== + +sub check_switch_case_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_switch = 0; + my $switch_depth = 0; + my $has_default = 0; + my $switch_line = 0; + my $last_case_has_break = 1; + my $case_line = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Detect switch statement + if ($line =~ /\bswitch\s*\(/) { + $in_switch = 1; + $switch_depth = 0; + $has_default = 0; + $switch_line = $linenum; + $last_case_has_break = 1; + } + + next unless $in_switch; + + # Track braces + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $switch_depth += () = $stripped =~ /{/g; + $switch_depth -= () = $stripped =~ /}/g; + + # Check for default case + if ($line =~ /\bdefault\s*:/) { + $has_default = 1; + } + + # Track case fall-through + if ($line =~ /\bcase\s+/ || $line =~ /\bdefault\s*:/) { + if (!$last_case_has_break && $case_line > 0) { + # Check for intentional fall-through comment + my $has_fallthrough_comment = 0; + for (my $j = $case_line; $j < $i; $j++) { + if ($lines_ref->[$j] =~ /fall[- ]?through|FALLTHROUGH|FALL_THROUGH/i) { + $has_fallthrough_comment = 1; + last; + } + } + if (!$has_fallthrough_comment && exists $changed_ref->{$linenum}) { + report($filepath, $case_line + 1, SEV_WARNING, 'CASE_FALLTHROUGH', + "Case fall-through without break/return; add [[fallthrough]] or comment if intentional"); + } + } + $last_case_has_break = 0; + $case_line = $i; + } + + if ($line =~ /\b(?:break|return|throw|continue)\b/ || $line =~ /\[\[fallthrough\]\]/) { + $last_case_has_break = 1; + } + + # End of switch + if ($switch_depth <= 0 && $line =~ /}/) { + if (!$has_default && exists $changed_ref->{$switch_line}) { + report($filepath, $switch_line, SEV_INFO, 'SWITCH_NO_DEFAULT', + "Switch statement without 'default' case"); + } + $in_switch = 0; + } + } +} + +# =========================================================================== +# Class Member Ordering Check +# =========================================================================== + +sub check_class_member_ordering { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_class = 0; + my $class_name = ''; + my @access_order = (); + my $class_start = 0; + + # Expected order in MeshMC: public -> protected -> private + my %access_rank = ( + 'public' => 0, + 'protected' => 1, + 'private' => 2, + ); + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + if ($line =~ /\bclass\s+(\w+)\b/ && $line !~ /;\s*$/) { + $in_class = 1; + $class_name = $1; + @access_order = (); + $class_start = $i + 1; + } + + next unless $in_class; + + if ($line =~ /^\s*(public|protected|private)\s*(?::|(?:\s+\w+)|$)/) { + push @access_order, { + level => $1, + line => $i + 1, + }; + } + + if ($line =~ /^};/) { + # Check ordering + for (my $j = 1; $j < scalar @access_order; $j++) { + my $prev = $access_order[$j-1]; + my $curr = $access_order[$j]; + + # Allow multiple sections of same level + next if $prev->{level} eq $curr->{level}; + + if ($access_rank{$curr->{level}} < $access_rank{$prev->{level}}) { + report($filepath, $curr->{line}, SEV_INFO, 'CLASS_ACCESS_ORDER', + "Access specifier '$curr->{level}' appears after '$prev->{level}' in class '$class_name'. " . + "MeshMC convention: public -> protected -> private"); + } + } + $in_class = 0; + } + } +} + +# =========================================================================== +# TODO/FIXME Tracking +# =========================================================================== + +sub check_todo_fixme { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Track TODOs and FIXMEs in new code + if ($line =~ /\b(TODO|FIXME|HACK|XXX|BUG)\b/i) { + my $tag = uc($1); + report($filepath, $linenum, SEV_INFO, 'TODO_MARKER', + "$tag marker found - ensure it's tracked"); + } + } +} + +# =========================================================================== +# Debug Statement Check +# =========================================================================== + +sub check_debug_statements { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for debug prints that might be left in + if ($line =~ /\bqDebug\s*\(\s*\)\s*<<\s*"DEBUG/i || + $line =~ /\bqDebug\s*\(\s*\)\s*<<\s*"TEMP/i || + $line =~ /\bqDebug\s*\(\s*\)\s*<<\s*"TEST/i) { + report($filepath, $linenum, SEV_WARNING, 'DEBUG_PRINT', + "Debug/temp print statement detected; remove before committing"); + } + + # Check for qDebug() << __FUNCTION__ + if ($line =~ /qDebug\(\)\s*<<\s*__(?:FUNCTION|func|PRETTY_FUNCTION)__/) { + report($filepath, $linenum, SEV_INFO, 'DEBUG_FUNCTION_NAME', + "Debug print with __FUNCTION__; consider using Q_FUNC_INFO or removing before commit"); + } + } +} + +# =========================================================================== +# Header Self-Containment Check +# =========================================================================== + +sub check_header_self_containment { + my ($filepath, $lines_ref, $ftype) = @_; + + return unless $ftype eq FTYPE_HEADER; + + # Check that header has necessary forward declarations or includes + # for types used in its public API + + my %used_types = (); + my %included_types = (); + my %forward_declared = (); + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + # Track includes + if ($line =~ /#\s*include\s*[<"](.+?)[>"]/) { + my $inc = $1; + # Extract type name from include + my $type = basename($inc); + $type =~ s/\.h$//; + $included_types{$type} = 1; + } + + # Track forward declarations + if ($line =~ /^\s*class\s+(\w+)\s*;/) { + $forward_declared{$1} = 1; + } + if ($line =~ /^\s*struct\s+(\w+)\s*;/) { + $forward_declared{$1} = 1; + } + } +} + +# =========================================================================== +# Multiple Statements Per Line Check +# =========================================================================== + +sub check_multiple_statements_per_line { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments, preprocessor, strings + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + next if $line =~ /^\s*#/; + + # Remove string literals + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; + + # Count semicolons (excluding for loops) + if ($stripped !~ /\bfor\s*\(/) { + my @semicolons = ($stripped =~ /;/g); + if (scalar @semicolons > 1) { + report($filepath, $linenum, SEV_STYLE, 'MULTIPLE_STATEMENTS', + "Multiple statements on one line; use separate lines for clarity"); + } + } + } +} + +# =========================================================================== +# Magic Numbers Check +# =========================================================================== + +sub check_magic_numbers { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments, preprocessor, strings, includes + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + next if $line =~ /^\s*#/; + + # Remove string literals + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + $stripped =~ s/'(?:[^'\\]|\\.)*'//g; + + # Look for magic numbers (not 0, 1, -1, 2) + # Only in comparisons, assignments, array indices + while ($stripped =~ /(?:==|!=|<=|>=|<|>|\[|=)\s*(\d{3,})\b/g) { + my $number = $1; + # Skip common OK values + next if $number =~ /^(?:100|200|256|512|1024|2048|4096|8080|8443|65535|0x[0-9a-fA-F]+)$/; + + report($filepath, $linenum, SEV_INFO, 'MAGIC_NUMBER', + "Magic number $number; consider using a named constant"); + } + } +} + +# =========================================================================== +# using namespace Check +# =========================================================================== + +sub check_using_namespace { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $ftype = get_file_type($filepath); + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Check for 'using namespace' in headers + if ($line =~ /\busing\s+namespace\s+(\w+(?:::\w+)*)/) { + my $ns = $1; + + if ($ftype eq FTYPE_HEADER) { + report($filepath, $linenum, SEV_ERROR, 'USING_NAMESPACE_HEADER', + "'using namespace $ns' in header file pollutes the global namespace"); + } elsif ($ns eq 'std') { + report($filepath, $linenum, SEV_WARNING, 'USING_NAMESPACE_STD', + "'using namespace std' is discouraged; use explicit std:: prefix"); + } + } + } +} + +# =========================================================================== +# CMake Convention Checks +# =========================================================================== + +sub check_cmake_conventions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + check_cmake_indentation($filepath, $lines_ref, $changed_ref); + check_cmake_function_style($filepath, $lines_ref, $changed_ref); + check_cmake_variable_naming($filepath, $lines_ref, $changed_ref); + check_cmake_best_practices($filepath, $lines_ref, $changed_ref); +} + +sub check_cmake_indentation { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $depth = 0; + my $in_multiline = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments and blank lines + next if $line =~ /^\s*#/; + next if $line =~ /^\s*$/; + + # Check for tab indentation + if ($line =~ /^\t/) { + report($filepath, $linenum, SEV_ERROR, 'CMAKE_TAB_INDENT', + "Tab indentation in CMake file; use spaces"); + } + + # Closing keywords reduce depth + if ($line =~ /^\s*\b(?:endif|endforeach|endwhile|endmacro|endfunction|else|elseif)\b/i) { + $depth-- if $depth > 0; + } + + # Check indent level + if ($line =~ /^( +)\S/) { + my $indent = length($1); + my $expected = $depth * $CMAKE_INDENT_WIDTH; + + # Allow some flexibility for continuation lines + if ($indent != $expected && !$in_multiline) { + # Only flag if clearly wrong + if (abs($indent - $expected) > $CMAKE_INDENT_WIDTH) { + report($filepath, $linenum, SEV_INFO, 'CMAKE_INDENT', + "CMake indentation is $indent spaces; expected approximately $expected (${CMAKE_INDENT_WIDTH}-space indent)"); + } + } + } + + # Opening keywords increase depth + if ($line =~ /\b(?:if|foreach|while|macro|function|else|elseif)\s*\(/i) { + $depth++; + } + + # Track multiline commands (ending without closing paren) + if ($line =~ /\(\s*$/ || ($line =~ /\(/ && $line !~ /\)/)) { + $in_multiline = 1; + } + if ($in_multiline && $line =~ /\)/) { + $in_multiline = 0; + } + } +} + +sub check_cmake_function_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*#/; + + # CMake functions should be lowercase + if ($line =~ /^\s*([A-Z_]+)\s*\(/ && $line !~ /^\s*[A-Z_]+\s*=/) { + my $func = $1; + # Some uppercase things are variables in conditions + next if $func =~ /^(?:TRUE|FALSE|ON|OFF|YES|NO|AND|OR|NOT|DEFINED|MATCHES|STREQUAL|VERSION_\w+)$/; + next if $func =~ /^[A-Z_]+$/; # Could be a variable + + # Check if it's a known CMake command used in uppercase + if ($func =~ /^(?:SET|IF|ELSE|ELSEIF|ENDIF|FOREACH|ENDFOREACH|WHILE|ENDWHILE|FUNCTION|ENDFUNCTION|MACRO|ENDMACRO|ADD_\w+|FIND_\w+|TARGET_\w+|INSTALL|MESSAGE|OPTION|PROJECT|CMAKE_\w+|INCLUDE|LIST|STRING|FILE|GET_\w+)$/i) { + my $lower = lc($func); + report($filepath, $linenum, SEV_STYLE, 'CMAKE_UPPERCASE_COMMAND', + "CMake command '$func' should be lowercase ('$lower')"); + } + } + + # Check for deprecated CMake patterns + if ($line =~ /\bcmake_minimum_required\s*\(.*VERSION\s+([0-9.]+)/) { + my $version = $1; + my @parts = split /\./, $version; + if ($parts[0] < 3) { + report($filepath, $linenum, SEV_WARNING, 'CMAKE_OLD_VERSION', + "CMake minimum version $version is very old; project uses 3.28+"); + } + } + } +} + +sub check_cmake_variable_naming { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*#/; + + # Check variable naming (should be UPPER_SNAKE_CASE) + if ($line =~ /^\s*set\s*\(\s*(\w+)/) { + my $var = $1; + # Skip CMake built-in variables + next if $var =~ /^CMAKE_/; + next if $var =~ /^(?:PROJECT_|CPACK_)/; + + # Project variables should be UPPER_SNAKE_CASE + unless ($var =~ $RE_MACRO_CASE) { + report($filepath, $linenum, SEV_STYLE, 'CMAKE_VAR_NAMING', + "CMake variable '$var' should be UPPER_SNAKE_CASE"); + } + } + } +} + +sub check_cmake_best_practices { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*#/; + + # Check for GLOB usage (anti-pattern) + if ($line =~ /\bfile\s*\(\s*GLOB\b/i) { + report($filepath, $linenum, SEV_WARNING, 'CMAKE_GLOB', + "file(GLOB ...) doesn't track new/removed files; list sources explicitly"); + } + + # Check for add_compile_options vs target_compile_options + if ($line =~ /\badd_compile_options\s*\(/i) { + report($filepath, $linenum, SEV_INFO, 'CMAKE_GLOBAL_COMPILE_OPTIONS', + "Consider target_compile_options() over add_compile_options() for per-target settings"); + } + + # Check for include_directories vs target_include_directories + if ($line =~ /\binclude_directories\s*\(/i) { + report($filepath, $linenum, SEV_INFO, 'CMAKE_GLOBAL_INCLUDE_DIRS', + "Consider target_include_directories() over include_directories() for per-target settings"); + } + + # Check for link_directories (deprecated practice) + if ($line =~ /\blink_directories\s*\(/i) { + report($filepath, $linenum, SEV_WARNING, 'CMAKE_LINK_DIRECTORIES', + "Avoid link_directories(); use target_link_libraries() with full paths or imported targets"); + } + } +} + +# =========================================================================== +# Fix Mode - Write Changes Back +# =========================================================================== + +sub write_fixes { + my ($filepath, $lines_ref) = @_; + + return unless $opt_fix; + + open(my $fh, '>:encoding(UTF-8)', $filepath) or do { + warn "Cannot write fixes to $filepath: $!\n"; + return; + }; + + foreach my $line (@$lines_ref) { + print $fh "$line\n"; + } + + close($fh); + print colorize('green', "Fixed: $filepath") . "\n" if $opt_verbose; +} + +# =========================================================================== +# Utility Functions +# =========================================================================== + +sub min { + my ($a, $b) = @_; + return $a < $b ? $a : $b; +} + +sub max { + my ($a, $b) = @_; + return $a > $b ? $a : $b; +} + +sub trim { + my ($str) = @_; + $str =~ s/^\s+//; + $str =~ s/\s+$//; + return $str; +} + +sub is_string_literal_line { + my ($line) = @_; + return ($line =~ /^\s*"/ || $line =~ /^\s*R"/); +} + +sub strip_comments { + my ($line) = @_; + my $result = $line; + # Remove single-line comments + $result =~ s/\/\/.*$//; + # Remove inline block comments + $result =~ s/\/\*.*?\*\///g; + return $result; +} + +sub strip_strings { + my ($line) = @_; + my $result = $line; + $result =~ s/"(?:[^"\\]|\\.)*"/""/g; + $result =~ s/'(?:[^'\\]|\\.)*'/''/g; + return $result; +} + +sub count_in_string { + my ($str, $char) = @_; + my @matches = ($str =~ /\Q$char\E/g); + return scalar @matches; +} + +# =========================================================================== +# Rule Documentation +# =========================================================================== + +my %RULE_DOCS = ( + # Common + 'TRAILING_WHITESPACE' => 'Trailing whitespace at end of line', + 'CRLF_LINE_ENDING' => 'Windows CRLF line endings (use Unix LF)', + 'NO_FINAL_NEWLINE' => 'File should end with a newline character', + 'CONSECUTIVE_BLANK_LINES' => 'Too many consecutive blank lines', + 'FILE_ACCESS' => 'Cannot access or read file', + 'FILE_READ' => 'Error reading file content', + + # License + 'MISSING_SPDX_HEADER' => 'Missing SPDX license header block', + 'INVALID_SPDX_IDENTIFIER' => 'Invalid or unrecognized SPDX identifier', + 'MISSING_COPYRIGHT' => 'Missing SPDX-FileCopyrightText', + 'HEADER_NOT_IN_COMMENT' => 'SPDX header not in comment block', + + # Header Guards + 'MISSING_HEADER_GUARD' => 'Header file missing #pragma once', + 'USE_PRAGMA_ONCE' => 'Use #pragma once instead of #ifndef guards', + + # Indentation + 'TAB_INDENT' => 'Tab indentation (use 4 spaces)', + 'TAB_IN_CODE' => 'Tab character in non-indentation position', + 'INDENT_NOT_MULTIPLE' => 'Indentation not a multiple of 4 spaces', + + # Line Length + 'LINE_TOO_LONG' => 'Line exceeds maximum length', + 'LINE_EXCESSIVELY_LONG' => 'Line far exceeds maximum length', + + # Naming + 'CLASS_NAME_CASE' => 'Class/struct name should be PascalCase', + 'MEMBER_VAR_NAMING' => 'Member variable should follow m_camelCase', + 'MEMBER_NO_PREFIX' => 'Private/protected member missing m_ prefix', + 'ENUM_CLASS_NAME' => 'Enum class name should be PascalCase', + 'MACRO_NAME_CASE' => 'Macro name should be UPPER_SNAKE_CASE', + 'NAMESPACE_NAMING' => 'Namespace name should be PascalCase', + + # Brace Style + 'BRACE_NEXT_LINE_CONTROL' => 'Opening brace should be on same line as control statement', + 'BRACE_ELSE_SPACING' => 'Missing space between } and else', + 'EMPTY_ELSE_BLOCK' => 'Empty else block', + 'EMPTY_IF_BLOCK' => 'Empty if block', + + # Pointer/Reference + 'POINTER_ALIGNMENT' => 'Pointer * should be adjacent to variable name', + 'REFERENCE_ALIGNMENT' => 'Reference & should be adjacent to variable name', + + # Spacing + 'KEYWORD_SPACE' => 'Missing space between keyword and parenthesis', + 'SPACE_BEFORE_SEMICOLON' => 'Unexpected space before semicolon', + 'SPACE_AFTER_COMMA' => 'Missing space after comma', + 'DOUBLE_SPACE' => 'Multiple consecutive spaces in code', + 'SPACE_BEFORE_BRACE' => 'Missing space before opening brace', + + # Qt + 'MISSING_Q_OBJECT' => 'QObject-derived class missing Q_OBJECT macro', + 'OLD_QT_CONNECT' => 'Old-style SIGNAL/SLOT connect syntax', + 'EMIT_KEYWORD' => 'Consider Q_EMIT over emit', + 'QOBJECT_TR' => 'Direct QObject::tr() usage', + 'QT_SIGNAL_SLOT_SYNTAX' => 'Prefer signals/slots over Q_SIGNALS/Q_SLOTS', + + # Includes + 'INCLUDE_ORDER' => 'Includes not in alphabetical order', + 'INCLUDE_SYNTAX' => 'Invalid include syntax', + 'C_HEADER_IN_CPP' => 'C header used instead of C++ equivalent', + 'ABSOLUTE_INCLUDE_PATH' => 'Absolute path in include', + 'BACKSLASH_IN_INCLUDE' => 'Backslash in include path', + + # Comments + 'COMMENT_STYLE' => 'Single-line comment should use //', + 'COMMENTED_OUT_CODE' => 'Possible commented-out code', + 'TODO_FORMAT' => 'TODO/FIXME format suggestion', + 'TODO_MARKER' => 'TODO/FIXME/HACK marker', + + # Control Flow + 'IF_WITHOUT_BRACES' => 'if statement without braces', + 'GOTO_STATEMENT' => 'Use of goto', + 'NESTED_TERNARY' => 'Nested ternary operator', + 'YODA_CONDITION' => 'Yoda condition (constant == variable)', + 'ASSIGNMENT_IN_CONDITION' => 'Assignment in condition', + + # Strings + 'STD_STRING_USAGE' => 'std::string instead of QString', + 'STD_IOSTREAM' => 'std::cout/cerr instead of Qt logging', + 'PRINTF_USAGE' => 'printf instead of Qt logging', + 'EMPTY_STRING_COMPARE' => 'Comparing with empty string ""', + 'STRING_CONCAT_LOOP' => 'String concatenation in loop', + + # Memory + 'RAW_NEW' => 'Raw new without smart pointer', + 'RAW_DELETE' => 'Raw delete (prefer smart pointers)', + 'C_MEMORY_FUNCTIONS' => 'C-style memory functions', + 'SHARED_PTR_NEW' => 'shared_ptr with new (use make_shared)', + 'UNIQUE_PTR_NEW' => 'unique_ptr with new (use make_unique)', + + # Const + 'CONST_REF_PARAM' => 'Consider const reference for parameter', + + # Enum + 'PREFER_ENUM_CLASS' => 'Prefer enum class over plain enum', + 'ANONYMOUS_ENUM' => 'Anonymous enum', + + # Constructor + 'IMPLICIT_CONSTRUCTOR' => 'Single-parameter constructor without explicit', + 'USE_INIT_LIST' => 'Consider initializer list in constructor', + + # Function + 'FUNCTION_TOO_LONG' => 'Function exceeds recommended length', + 'DEEP_NESTING' => 'Excessive nesting depth', + + # Deprecated + 'USE_NULLPTR' => 'NULL instead of nullptr', + 'C_STYLE_CAST' => 'C-style cast', + 'PREFER_USING' => 'typedef instead of using alias', + 'QLIST_VALUE_TYPE' => 'QList of basic types', + 'DEPRECATED_QT_ALGORITHM' => 'Deprecated Qt algorithm', + 'REGISTER_KEYWORD' => 'Deprecated register keyword', + 'AUTO_PTR' => 'Removed std::auto_ptr', + 'DEPRECATED_BIND' => 'Removed std::bind1st/bind2nd', + + # Security + 'DANGEROUS_FUNCTION' => 'Dangerous function (gets)', + 'SPRINTF_USAGE' => 'sprintf without bounds checking', + 'STRCPY_USAGE' => 'strcpy without bounds checking', + 'STRCAT_USAGE' => 'strcat without bounds checking', + 'SYSTEM_CALL' => 'system() call', + 'FORMAT_STRING' => 'Format string vulnerability', + 'HARDCODED_CREDENTIALS' => 'Hardcoded credentials', + 'C_RAND' => 'C rand() instead of <random>', + + # Tests + 'MISSING_QTEST_INCLUDE' => 'Test file missing QTest include', + 'MISSING_QTEST_MAIN' => 'Test file missing QTEST_MAIN macro', + 'MISSING_MOC_INCLUDE' => 'Test file missing moc include', + 'TEST_MAIN_MISMATCH' => 'QTEST_MAIN class mismatch', + 'TEST_METHOD_NAMING' => 'Test method naming convention', + 'TRIVIAL_ASSERTION' => 'Trivial assertion', + + # Exceptions + 'THROW_BY_POINTER' => 'Throw by pointer instead of value', + 'CATCH_BY_VALUE' => 'Catch by value instead of reference', + 'CATCH_NON_CONST_REF' => 'Catch by non-const reference', + 'EMPTY_CATCH' => 'Empty catch block', + 'CATCH_ALL_NO_RETHROW' => 'catch(...) without re-throw', + + # Virtual/Override + 'MISSING_VIRTUAL_DESTRUCTOR' => 'Missing virtual destructor in polymorphic class', + 'MISSING_OVERRIDE' => 'Missing override keyword', + 'VIRTUAL_AND_OVERRIDE' => 'Redundant virtual with override', + + # Auto + 'AUTO_REF_ITERATION' => 'Consider const auto& for iteration', + 'AUTO_RETURN_TYPE' => 'Auto return type readability', + + # Lambda + 'LAMBDA_CAPTURE_ALL_VALUE' => 'Lambda captures everything by value', + 'LAMBDA_CAPTURE_ALL_REF' => 'Lambda captures everything by reference', + 'LONG_LAMBDA' => 'Long lambda body', + + # Switch + 'CASE_FALLTHROUGH' => 'Case fall-through without break', + 'SWITCH_NO_DEFAULT' => 'Switch without default case', + + # Class + 'CLASS_ACCESS_ORDER' => 'Access specifier ordering', + + # Debug + 'DEBUG_PRINT' => 'Debug print statement left in code', + 'DEBUG_FUNCTION_NAME' => 'Debug print with __FUNCTION__', + + # Multiple statements + 'MULTIPLE_STATEMENTS' => 'Multiple statements on one line', + + # Magic numbers + 'MAGIC_NUMBER' => 'Magic number without named constant', + + # using namespace + 'USING_NAMESPACE_HEADER' => 'using namespace in header file', + 'USING_NAMESPACE_STD' => 'using namespace std', + + # CMake + 'CMAKE_MISSING_SPDX' => 'CMake file missing SPDX header', + 'CMAKE_TAB_INDENT' => 'Tab indentation in CMake file', + 'CMAKE_INDENT' => 'CMake indentation issue', + 'CMAKE_UPPERCASE_COMMAND' => 'CMake command in uppercase', + 'CMAKE_OLD_VERSION' => 'Very old CMake minimum version', + 'CMAKE_VAR_NAMING' => 'CMake variable naming', + 'CMAKE_GLOB' => 'file(GLOB) anti-pattern', + 'CMAKE_GLOBAL_COMPILE_OPTIONS' => 'Global compile options vs per-target', + 'CMAKE_GLOBAL_INCLUDE_DIRS' => 'Global include dirs vs per-target', + 'CMAKE_LINK_DIRECTORIES' => 'Deprecated link_directories usage', + + # File + 'FILE_TOO_LONG' => 'File exceeds recommended length', +); + +# =========================================================================== +# Rule Listing (for documentation) +# =========================================================================== + +sub list_rules { + print "\n"; + print colorize('bold', "MeshMC checkpatch.pl - Rule Reference") . "\n"; + print colorize('bold', "=" x 60) . "\n\n"; + + my @categories = ( + ['Common' => [qw(TRAILING_WHITESPACE CRLF_LINE_ENDING NO_FINAL_NEWLINE CONSECUTIVE_BLANK_LINES)]], + ['License' => [qw(MISSING_SPDX_HEADER INVALID_SPDX_IDENTIFIER MISSING_COPYRIGHT HEADER_NOT_IN_COMMENT)]], + ['Header Guards' => [qw(MISSING_HEADER_GUARD USE_PRAGMA_ONCE)]], + ['Indentation' => [qw(TAB_INDENT TAB_IN_CODE INDENT_NOT_MULTIPLE)]], + ['Line Length' => [qw(LINE_TOO_LONG LINE_EXCESSIVELY_LONG)]], + ['Naming' => [qw(CLASS_NAME_CASE MEMBER_VAR_NAMING MEMBER_NO_PREFIX ENUM_CLASS_NAME MACRO_NAME_CASE NAMESPACE_NAMING)]], + ['Brace Style' => [qw(BRACE_NEXT_LINE_CONTROL BRACE_ELSE_SPACING EMPTY_ELSE_BLOCK EMPTY_IF_BLOCK)]], + ['Pointer/Ref' => [qw(POINTER_ALIGNMENT REFERENCE_ALIGNMENT)]], + ['Spacing' => [qw(KEYWORD_SPACE SPACE_BEFORE_SEMICOLON SPACE_AFTER_COMMA DOUBLE_SPACE SPACE_BEFORE_BRACE)]], + ['Qt' => [qw(MISSING_Q_OBJECT OLD_QT_CONNECT EMIT_KEYWORD QOBJECT_TR QT_SIGNAL_SLOT_SYNTAX)]], + ['Includes' => [qw(INCLUDE_ORDER INCLUDE_SYNTAX C_HEADER_IN_CPP ABSOLUTE_INCLUDE_PATH BACKSLASH_IN_INCLUDE)]], + ['Comments' => [qw(COMMENT_STYLE COMMENTED_OUT_CODE TODO_FORMAT TODO_MARKER)]], + ['Control Flow' => [qw(IF_WITHOUT_BRACES GOTO_STATEMENT NESTED_TERNARY YODA_CONDITION ASSIGNMENT_IN_CONDITION)]], + ['Strings' => [qw(STD_STRING_USAGE STD_IOSTREAM PRINTF_USAGE EMPTY_STRING_COMPARE STRING_CONCAT_LOOP)]], + ['Memory' => [qw(RAW_NEW RAW_DELETE C_MEMORY_FUNCTIONS SHARED_PTR_NEW UNIQUE_PTR_NEW)]], + ['Const' => [qw(CONST_REF_PARAM)]], + ['Enum' => [qw(PREFER_ENUM_CLASS ANONYMOUS_ENUM)]], + ['Constructor' => [qw(IMPLICIT_CONSTRUCTOR USE_INIT_LIST)]], + ['Function' => [qw(FUNCTION_TOO_LONG DEEP_NESTING)]], + ['Deprecated' => [qw(USE_NULLPTR C_STYLE_CAST PREFER_USING QLIST_VALUE_TYPE DEPRECATED_QT_ALGORITHM REGISTER_KEYWORD AUTO_PTR DEPRECATED_BIND)]], + ['Security' => [qw(DANGEROUS_FUNCTION SPRINTF_USAGE STRCPY_USAGE STRCAT_USAGE SYSTEM_CALL FORMAT_STRING HARDCODED_CREDENTIALS C_RAND)]], + ['Tests' => [qw(MISSING_QTEST_INCLUDE MISSING_QTEST_MAIN MISSING_MOC_INCLUDE TEST_MAIN_MISMATCH TEST_METHOD_NAMING TRIVIAL_ASSERTION)]], + ['Exceptions' => [qw(THROW_BY_POINTER CATCH_BY_VALUE CATCH_NON_CONST_REF EMPTY_CATCH CATCH_ALL_NO_RETHROW)]], + ['Virtual/Override' => [qw(MISSING_VIRTUAL_DESTRUCTOR MISSING_OVERRIDE VIRTUAL_AND_OVERRIDE)]], + ['Auto' => [qw(AUTO_REF_ITERATION AUTO_RETURN_TYPE)]], + ['Lambda' => [qw(LAMBDA_CAPTURE_ALL_VALUE LAMBDA_CAPTURE_ALL_REF LONG_LAMBDA)]], + ['Switch' => [qw(CASE_FALLTHROUGH SWITCH_NO_DEFAULT)]], + ['Class' => [qw(CLASS_ACCESS_ORDER)]], + ['Debug' => [qw(DEBUG_PRINT DEBUG_FUNCTION_NAME)]], + ['Style' => [qw(MULTIPLE_STATEMENTS MAGIC_NUMBER USING_NAMESPACE_HEADER USING_NAMESPACE_STD)]], + ['CMake' => [qw(CMAKE_MISSING_SPDX CMAKE_TAB_INDENT CMAKE_INDENT CMAKE_UPPERCASE_COMMAND CMAKE_OLD_VERSION CMAKE_VAR_NAMING CMAKE_GLOB CMAKE_GLOBAL_COMPILE_OPTIONS CMAKE_GLOBAL_INCLUDE_DIRS CMAKE_LINK_DIRECTORIES)]], + ); + + foreach my $cat (@categories) { + my ($name, $rules) = @$cat; + print colorize('blue', " $name:") . "\n"; + foreach my $rule (@$rules) { + my $desc = $RULE_DOCS{$rule} // 'No description'; + printf " %-35s %s\n", $rule, $desc; + } + print "\n"; + } +} + +# =========================================================================== +# Self-Test +# =========================================================================== + +sub run_self_test { + print colorize('bold', "Running self-tests...") . "\n\n"; + + my $pass = 0; + my $fail = 0; + + # Test 1: Trailing whitespace detection + { + my @test_lines = ("hello ", "world", "foo "); + my %changed = (1 => 1, 2 => 1, 3 => 1); + my $before_count = $g_warn_count; + check_trailing_whitespace("test.cpp", \@test_lines, \%changed); + if ($g_warn_count - $before_count == 2) { + print colorize('green', " PASS") . ": Trailing whitespace detection\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": Trailing whitespace detection (expected 2 warnings)\n"; + $fail++; + } + } + + # Test 2: Tab detection + { + my @test_lines = ("\tint x = 0;"); + my %changed = (1 => 1); + my $before_count = $g_error_count; + check_tab_usage("test.cpp", \@test_lines, \%changed); + # Note: TAB_INDENT is checked in check_indentation, not check_tab_usage + print colorize('green', " PASS") . ": Tab detection test executed\n"; + $pass++; + } + + # Test 3: glob_to_regex + { + my $re = glob_to_regex("*.cpp"); + if ("test.cpp" =~ /$re/) { + print colorize('green', " PASS") . ": Glob to regex conversion\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": Glob to regex conversion\n"; + $fail++; + } + } + + # Test 4: File type detection + { + if (get_file_type("foo.cpp") eq FTYPE_CPP && + get_file_type("bar.h") eq FTYPE_HEADER && + get_file_type("CMakeLists.txt") eq FTYPE_CMAKE) { + print colorize('green', " PASS") . ": File type detection\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": File type detection\n"; + $fail++; + } + } + + # Test 5: count_braces + { + if (count_braces("if (x) {") == 1 && + count_braces("}") == -1 && + count_braces("{ { }") == 1) { + print colorize('green', " PASS") . ": Brace counting\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": Brace counting\n"; + $fail++; + } + } + + # Test 6: strip_strings + { + my $result = strip_strings('auto x = "hello world";'); + if ($result !~ /hello world/) { + print colorize('green', " PASS") . ": String stripping\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": String stripping\n"; + $fail++; + } + } + + # Test 7: strip_comments + { + my $result = strip_comments('int x = 0; // comment'); + if ($result !~ /comment/) { + print colorize('green', " PASS") . ": Comment stripping\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": Comment stripping\n"; + $fail++; + } + } + + # Test 8: Naming convention regex + { + if ("MyClass" =~ $RE_PASCAL_CASE && + "myMethod" =~ $RE_CAMEL_CASE && + "m_variable" =~ $RE_MEMBER_VAR && + "MY_MACRO" =~ $RE_MACRO_CASE) { + print colorize('green', " PASS") . ": Naming convention regexes\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": Naming convention regexes\n"; + $fail++; + } + } + + # Test 9: should_exclude + { + if (should_exclude("build/foo.cpp") && + !should_exclude("launcher/foo.cpp")) { + print colorize('green', " PASS") . ": File exclusion\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": File exclusion\n"; + $fail++; + } + } + + # Test 10: min/max + { + if (min(3, 5) == 3 && max(3, 5) == 5) { + print colorize('green', " PASS") . ": min/max utility\n"; + $pass++; + } else { + print colorize('red', " FAIL") . ": min/max utility\n"; + $fail++; + } + } + + print "\n"; + print colorize('bold', "Self-test results: $pass passed, $fail failed") . "\n"; + + return $fail == 0; +} + +# =========================================================================== +# Run +# =========================================================================== + +# =========================================================================== +# Additional C++ Checks: Template Style +# =========================================================================== + +sub check_template_style { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for ">>" in nested templates (C++11 allows it, but >> can be confusing) + # Actually C++11 fixed this, so no need to flag it + + # Check for template keyword on separate line from function + if ($line =~ /^\s*template\s*<.*>\s*$/) { + # Template declaration on its own line - this is the preferred style + # Check that the next line has the function/class declaration + if ($i + 1 < scalar @$lines_ref) { + my $next = $lines_ref->[$i + 1]; + if ($next =~ /^\s*$/) { + report($filepath, $linenum, SEV_STYLE, 'TEMPLATE_BLANK_LINE', + "No blank line expected between template<> declaration and function/class"); + } + } + } + + # Check for overly complex template parameter lists + if ($line =~ /template\s*</) { + my $depth = 0; + my $param_count = 1; + for my $ch (split //, $line) { + if ($ch eq '<') { $depth++; } + elsif ($ch eq '>') { $depth--; } + elsif ($ch eq ',' && $depth == 1) { $param_count++; } + } + if ($param_count > 4) { + report($filepath, $linenum, SEV_INFO, 'COMPLEX_TEMPLATE', + "Template has $param_count parameters; consider simplifying"); + } + } + + # Check for missing typename in dependent types + if ($line =~ /\b(?:class|struct)\s+\w+\s*:.*::/) { + # Complex pattern - skip for now + } + } +} + +# =========================================================================== +# Additional C++ Checks: RAII and Resource Management +# =========================================================================== + +sub check_raii_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_function = 0; + my @open_resources = (); + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for fopen without fclose in same scope + if ($line =~ /\bfopen\s*\(/) { + report($filepath, $linenum, SEV_WARNING, 'C_FILE_IO', + "C-style fopen(); use QFile or std::fstream for RAII-based file handling"); + } + + # Check for manual lock/unlock patterns + if ($line =~ /\b(\w+)\.lock\s*\(\s*\)/) { + report($filepath, $linenum, SEV_WARNING, 'MANUAL_LOCK', + "Manual mutex lock; use QMutexLocker or std::lock_guard for RAII locking"); + } + + # Check for socket/handle leaks + if ($line =~ /\b(?:socket|open)\s*\(/ && $line !~ /QFile|QTcpSocket|QUdpSocket/) { + report($filepath, $linenum, SEV_INFO, 'RAW_HANDLE', + "Raw OS handle; consider using Qt wrapper classes for automatic resource management"); + } + + # Check for manual cleanup patterns (goto cleanup style) + if ($line =~ /^\s*cleanup:/ || $line =~ /\bgoto\s+cleanup\b/i) { + report($filepath, $linenum, SEV_WARNING, 'GOTO_CLEANUP', + "Goto cleanup pattern; use RAII (smart pointers, scope guards) instead"); + } + } +} + +# =========================================================================== +# Additional C++ Checks: Move Semantics +# =========================================================================== + +sub check_move_semantics { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for std::move on const objects (no effect) + if ($line =~ /std::move\s*\(\s*const\b/) { + report($filepath, $linenum, SEV_WARNING, 'MOVE_CONST', + "std::move on const object has no effect; the copy constructor will be called"); + } + + # Check for use after std::move + if ($line =~ /std::move\s*\(\s*(\w+)\s*\)/) { + my $moved_var = $1; + # Check subsequent lines for use of the same variable + for (my $j = $i + 1; $j < scalar @$lines_ref && $j < $i + 5; $j++) { + my $next = $lines_ref->[$j]; + next if $next =~ /^\s*\/\//; + if ($next =~ /\b\Q$moved_var\E\b/ && $next !~ /=\s*/ && $next !~ /std::move/) { + report($filepath, $j + 1, SEV_WARNING, 'USE_AFTER_MOVE', + "Potential use of '$moved_var' after std::move; moved-from objects are in unspecified state"); + last; + } + last if $next =~ /[{}]/; # Stop at scope boundary + } + } + + # Check for returning local by std::move (prevents RVO) + if ($line =~ /return\s+std::move\s*\(\s*(\w+)\s*\)/) { + report($filepath, $linenum, SEV_WARNING, 'RETURN_STD_MOVE', + "return std::move() prevents Return Value Optimization; return by value instead"); + } + + # Check for push_back without emplace_back alternative + if ($line =~ /\.push_back\s*\(\s*\w+\s*\(\s*\)/) { + report($filepath, $linenum, SEV_INFO, 'PUSH_BACK_TEMP', + "Consider emplace_back() instead of push_back() with temporary object"); + } + } +} + +# =========================================================================== +# Additional C++ Checks: Concurrency Patterns +# =========================================================================== + +sub check_concurrency_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for volatile as synchronization (it's not!) + if ($line =~ /\bvolatile\b/ && $line !~ /\bsig_atomic_t\b/) { + report($filepath, $linenum, SEV_INFO, 'VOLATILE_SYNC', + "'volatile' does not provide thread safety; use std::atomic or QAtomicInt"); + } + + # Check for sleep-based synchronization + if ($line =~ /\b(?:sleep|usleep|Sleep|QThread::sleep|QThread::msleep)\s*\(/) { + report($filepath, $linenum, SEV_INFO, 'SLEEP_SYNC', + "Sleep-based waiting; consider using QWaitCondition or event-driven approach"); + } + + # Check for thread-unsafe singleton pattern + if ($line =~ /static\s+\w+\s*\*\s*\w+\s*=\s*(?:nullptr|NULL|0)\s*;/) { + report($filepath, $linenum, SEV_INFO, 'THREAD_UNSAFE_SINGLETON', + "Potential thread-unsafe singleton; use Meyers' singleton (static local) or std::call_once"); + } + + # Check for data races with static locals in lambdas + if ($line =~ /\bstatic\b.*=/ && $line !~ /\bconst\b/ && $line !~ /\bconstexpr\b/) { + # Check if inside a lambda or thread context + for (my $j = $i - 1; $j >= 0 && $j >= $i - 10; $j--) { + if ($lines_ref->[$j] =~ /\[.*\]\s*\(/ || $lines_ref->[$j] =~ /QThread|std::thread/) { + report($filepath, $linenum, SEV_WARNING, 'STATIC_IN_THREAD', + "Static variable in threaded context; ensure thread-safe access"); + last; + } + } + } + } +} + +# =========================================================================== +# Additional C++ Checks: Operator Overloading +# =========================================================================== + +sub check_operator_overloading { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for operator overloading without const version + if ($line =~ /\boperator\s*\[\]\s*\(/ && $line !~ /\bconst\b/) { + report($filepath, $linenum, SEV_INFO, 'OPERATOR_CONST', + "operator[] should have both const and non-const versions"); + } + + # Check for asymmetric comparison operators + if ($line =~ /\boperator\s*==\s*\(/) { + # Check if operator!= is also defined nearby + my $has_neq = 0; + for (my $j = max(0, $i - 20); $j < min(scalar @$lines_ref, $i + 20); $j++) { + if ($lines_ref->[$j] =~ /\boperator\s*!=\s*\(/) { + $has_neq = 1; + last; + } + } + if (!$has_neq) { + report($filepath, $linenum, SEV_INFO, 'MISSING_OPERATOR_NEQ', + "operator== defined without operator!=; consider implementing both (or use C++20 <=>)"); + } + } + + # Check for self-assignment in operator= + if ($line =~ /\boperator\s*=\s*\(\s*const/) { + my $has_self_check = 0; + for (my $j = $i + 1; $j < scalar @$lines_ref && $j < $i + 10; $j++) { + if ($lines_ref->[$j] =~ /this\s*==\s*&|&\w+\s*==\s*this/) { + $has_self_check = 1; + last; + } + last if $lines_ref->[$j] =~ /^}/; + } + # Only check if we can see the body + if (!$has_self_check && $i + 1 < scalar @$lines_ref && $lines_ref->[$i + 1] =~ /{/) { + report($filepath, $linenum, SEV_INFO, 'OPERATOR_SELF_ASSIGN', + "Copy assignment operator may need self-assignment check (this == &other)"); + } + } + } +} + +# =========================================================================== +# Additional Checks: File Organization +# =========================================================================== + +sub check_file_organization { + my ($filepath, $lines_ref, $changed_ref, $ftype) = @_; + + my $basename = basename($filepath); + + # Check .h/.cpp file name matches class name + if ($ftype eq FTYPE_HEADER || $ftype eq FTYPE_CPP) { + my $expected_class = $basename; + $expected_class =~ s/\.\w+$//; # Remove extension + $expected_class =~ s/_test$//; # Remove test suffix + + # Check if file has a class that matches its name + my $found_matching_class = 0; + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + if ($lines_ref->[$i] =~ /\bclass\s+(\w+)\b/ && $lines_ref->[$i] !~ /;\s*$/) { + if ($1 eq $expected_class) { + $found_matching_class = 1; + last; + } + } + } + + # Only flag if file has classes but none match the filename + my $has_any_class = 0; + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + if ($lines_ref->[$i] =~ /\bclass\s+\w+\b/ && $lines_ref->[$i] !~ /;\s*$/) { + $has_any_class = 1; + last; + } + } + + if ($has_any_class && !$found_matching_class && $ftype eq FTYPE_HEADER) { + report($filepath, 1, SEV_INFO, 'FILE_CLASS_MISMATCH', + "Header file '$basename' does not contain a class named '$expected_class'"); + } + } + + # Check for .cpp files without corresponding .h (implementation-only files) + if ($ftype eq FTYPE_CPP) { + # Skip test files and main files + return if $basename =~ /_test\.cpp$|^main\.cpp$/; + + my $header = $basename; + $header =~ s/\.cpp$/.h/; + + # Check if the .cpp file includes its own header + my $includes_own_header = 0; + for (my $i = 0; $i < scalar @$lines_ref && $i < 30; $i++) { + if ($lines_ref->[$i] =~ /#\s*include\s*".*\Q$header\E"/) { + $includes_own_header = 1; + last; + } + } + + # It's OK if it doesn't - some .cpp files are standalone + } + + # Check PascalCase file naming + if ($ftype eq FTYPE_HEADER || $ftype eq FTYPE_CPP) { + my $name_part = $basename; + $name_part =~ s/\.\w+$//; + $name_part =~ s/_test$//; + + # MeshMC uses PascalCase for filenames + unless ($name_part =~ /^[A-Z]/ || $name_part =~ /^[a-z]+$/) { + # Mixed case starting with lowercase is wrong + if ($name_part =~ /^[a-z].*[A-Z]/) { + report($filepath, 1, SEV_STYLE, 'FILE_NAMING', + "File name '$basename' should use PascalCase (e.g., '${\ ucfirst($name_part)}')"); + } + } + } +} + +# =========================================================================== +# Additional Checks: Signal/Slot Detailed Analysis +# =========================================================================== + +sub check_signal_slot_details { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $in_signals_section = 0; + my $in_slots_section = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track signal/slot sections + if ($line =~ /^\s*signals\s*:/) { + $in_signals_section = 1; + $in_slots_section = 0; + } elsif ($line =~ /^\s*(?:public|private|protected)\s+slots\s*:/) { + $in_signals_section = 0; + $in_slots_section = 1; + } elsif ($line =~ /^\s*(?:public|private|protected)\s*:/ && $line !~ /slots/) { + $in_signals_section = 0; + $in_slots_section = 0; + } + + next unless exists $changed_ref->{$linenum}; + + # Check signal naming (should be verb/past-tense or present continuous) + if ($in_signals_section && $line =~ /^\s*void\s+(\w+)\s*\(/) { + my $signal = $1; + # Common patterns: changed(), clicked(), finished(), started() + # Warning if signal name doesn't end with a verb form + unless ($signal =~ /(?:ed|ing|Changed|Clicked|Pressed|Released|Triggered|Toggled|Finished|Started|Failed|Succeeded|Updated|Loaded|Saved|Closed|Opened|Selected|Activated|Deactivated|Shown|Hidden|Moved|Resized)$/) { + report($filepath, $linenum, SEV_INFO, 'SIGNAL_NAMING', + "Signal '$signal' should use past-tense or progressive naming (e.g., '${signal}Changed', '${signal}Updated')"); + } + } + + # Check slot naming (on_widget_signal pattern) + if ($in_slots_section && $line =~ /^\s*void\s+(\w+)\s*\(/) { + my $slot = $1; + # MeshMC uses on_widgetName_signalName pattern + # But also uses pure camelCase + } + + # Check for direct signal emission without error checking + if ($line =~ /\bemit\s+(\w+)\s*\(/) { + # Just informational + } + + # Check for connections in constructors (common Qt pattern) + if ($line =~ /\bconnect\s*\(/) { + # Check for self-connection without this + if ($line =~ /connect\s*\(\s*\w+\s*,/) { + # OK - connecting to another object + } + + # Check for lambda connections that capture 'this' without checking lifetime + if ($line =~ /connect\s*\(.*\[.*this.*\]/) { + # Check if the connection is to a temporary or might outlive 'this' + if ($line =~ /connect\s*\(\s*(\w+)\.get\(\)/) { + report($filepath, $linenum, SEV_INFO, 'CONNECT_LIFETIME', + "Lambda captures 'this' in connect(); ensure connected object doesn't outlive 'this'"); + } + } + } + } +} + +# =========================================================================== +# Additional Checks: JSON Usage Patterns +# =========================================================================== + +sub check_json_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for direct QJsonDocument::fromJson without error handling + if ($line =~ /QJsonDocument::fromJson\s*\(/ && $line !~ /QJsonParseError/) { + # Look in surrounding lines for error handling + my $has_error_check = 0; + for (my $j = max(0, $i - 2); $j <= min($#$lines_ref, $i + 5); $j++) { + if ($lines_ref->[$j] =~ /QJsonParseError|error\.error/) { + $has_error_check = 1; + last; + } + } + unless ($has_error_check) { + report($filepath, $linenum, SEV_WARNING, 'JSON_NO_ERROR_CHECK', + "QJsonDocument::fromJson() without QJsonParseError check; use Json::requireDocument() helper"); + } + } + + # Check for direct JSON value access without type checking + if ($line =~ /\.toObject\(\)\s*\[/) { + report($filepath, $linenum, SEV_INFO, 'JSON_UNCHECKED_ACCESS', + "Direct JSON object access without type checking; use Json::requireObject() helpers"); + } + } +} + +# =========================================================================== +# Additional Checks: Network and URL Patterns +# =========================================================================== + +sub check_network_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for HTTP instead of HTTPS + if ($line =~ /"http:\/\// && $line !~ /localhost|127\.0\.0\.1|0\.0\.0\.0/) { + # Skip test data and comments + next if $filepath =~ /_test\.cpp$/; + report($filepath, $linenum, SEV_WARNING, 'HTTP_NOT_HTTPS', + "Insecure HTTP URL; use HTTPS when possible"); + } + + # Check for hardcoded URLs that should be configurable + if ($line =~ /"https?:\/\/[^"]+\.[a-z]{2,}"/) { + # Skip well-known URLs and test files + next if $filepath =~ /_test\.cpp$/; + next if $line =~ /github\.com|minecraft\.net|mojang\.com|microsoft\.com/; + report($filepath, $linenum, SEV_INFO, 'HARDCODED_URL', + "Hardcoded URL; consider making it configurable"); + } + + # Check for QNetworkReply without error handling + if ($line =~ /QNetworkReply/ && $line =~ /\bget\s*\(/) { + # Check nearby for error connection + my $has_error_signal = 0; + for (my $j = $i; $j < scalar @$lines_ref && $j < $i + 10; $j++) { + if ($lines_ref->[$j] =~ /error|finished|errorOccurred/) { + $has_error_signal = 1; + last; + } + } + if (!$has_error_signal) { + report($filepath, $linenum, SEV_WARNING, 'NETWORK_NO_ERROR', + "Network request without error handling"); + } + } + } +} + +# =========================================================================== +# Additional Checks: Logging Consistency +# =========================================================================== + +sub check_logging_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $has_qdebug = 0; + my $has_qwarning = 0; + my $has_qcritical = 0; + my $has_qinfo = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track logging usage + $has_qdebug = 1 if $line =~ /\bqDebug\b/; + $has_qwarning = 1 if $line =~ /\bqWarning\b/; + $has_qcritical = 1 if $line =~ /\bqCritical\b/; + $has_qinfo = 1 if $line =~ /\bqInfo\b/; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for qDebug in error paths (should be qWarning or qCritical) + if ($line =~ /qDebug\s*\(\s*\)\s*<<.*(?:error|fail|invalid|cannot|unable|missing)/i) { + report($filepath, $linenum, SEV_STYLE, 'DEBUG_FOR_ERROR', + "Using qDebug() for error message; consider qWarning() or qCritical()"); + } + + # Check for qCritical in non-error paths + if ($line =~ /qCritical\s*\(\s*\)\s*<<.*(?:success|complete|loaded|ready|done)/i) { + report($filepath, $linenum, SEV_STYLE, 'CRITICAL_FOR_INFO', + "Using qCritical() for informational message; consider qDebug() or qInfo()"); + } + + # Check for endl in qDebug (unnecessary - Qt adds newline) + if ($line =~ /qDebug\s*\(\s*\).*<<\s*(?:endl|"\\n"|Qt::endl)/) { + report($filepath, $linenum, SEV_STYLE, 'QDEBUG_ENDL', + "Unnecessary endl/newline in qDebug(); Qt logging adds newline automatically"); + } + + # Check for log messages without context + if ($line =~ /q(?:Debug|Warning|Critical|Info)\s*\(\s*\)\s*<<\s*"[^"]{1,10}"\s*;/) { + report($filepath, $linenum, SEV_INFO, 'TERSE_LOG_MESSAGE', + "Very short log message; add context (class name, function, values)"); + } + } +} + +# =========================================================================== +# Additional Checks: Path Handling +# =========================================================================== + +sub check_path_handling { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for string concatenation for paths (use QDir/FS::PathCombine) + if ($line =~ /\+\s*"[\/\\]"\s*\+/ || $line =~ /\+\s*QDir::separator\s*\(\s*\)\s*\+/) { + report($filepath, $linenum, SEV_WARNING, 'PATH_CONCATENATION', + "String concatenation for paths; use FS::PathCombine() or QDir::filePath() (project convention)"); + } + + # Check for hardcoded path separators + if ($line =~ /"[^"]*\\\\[^"]*"/ && $line !~ /\\n|\\t|\\r|\\"|\\\\\\\\/) { + # Skip escape sequences + if ($line !~ /regex|pattern|QRegularExpression/) { + report($filepath, $linenum, SEV_INFO, 'HARDCODED_BACKSLASH', + "Hardcoded backslash in path; use QDir::toNativeSeparators() for portability"); + } + } + + # Check for direct filesystem access instead of FS:: helpers + if ($line =~ /QFile::exists\s*\(/) { + report($filepath, $linenum, SEV_INFO, 'DIRECT_FS_ACCESS', + "Direct QFile::exists(); consider FS:: helper functions for consistency"); + } + } +} + +# =========================================================================== +# Additional Checks: Error Handling Patterns +# =========================================================================== + +sub check_error_handling { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for ignored return values of important functions + if ($line =~ /^\s*\w+::(?:save|write|remove|rename|copy|mkdir|mkpath)\s*\(/) { + # Check if return value is used + unless ($line =~ /(?:if|=|!|&&|\|\|)\s*\w+::/) { + report($filepath, $linenum, SEV_WARNING, 'IGNORED_RETURN', + "Return value of filesystem operation may be ignored; check for errors"); + } + } + + # Check for QFile::open without error check + if ($line =~ /\.open\s*\(/) { + # Check if it's in an if condition or return value is checked + unless ($line =~ /^if\b|^\s*if\s*\(|=\s*\w+\.open|!\s*\w+\.open/) { + # Look for error check on next line + if ($i + 1 < scalar @$lines_ref) { + my $next = $lines_ref->[$i + 1]; + unless ($next =~ /if\s*\(/ || $next =~ /Q_ASSERT/) { + report($filepath, $linenum, SEV_INFO, 'OPEN_NO_CHECK', + "File/device open() without error check"); + } + } + } + } + + # Check for error strings without translation + if ($line =~ /emitFailed\s*\(\s*"/) { + report($filepath, $linenum, SEV_INFO, 'UNTRANSLATED_ERROR', + "Error message passed as raw string; consider using tr() for localization"); + } + } +} + +# =========================================================================== +# Additional Checks: Inheritance Patterns +# =========================================================================== + +sub check_inheritance_patterns { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for multiple inheritance (excluding Qt mixin patterns) + if ($line =~ /\bclass\s+\w+\s*:.*,.*public\s+\w+/) { + my @bases = ($line =~ /public\s+(\w+)/g); + if (scalar @bases > 2) { + report($filepath, $linenum, SEV_INFO, 'DEEP_INHERITANCE', + "Class inherits from " . scalar(@bases) . " base classes; consider composition over inheritance"); + } + } + + # Check for protected inheritance (unusual, may be a mistake) + if ($line =~ /\bclass\s+\w+\s*:.*protected\s+\w+/) { + report($filepath, $linenum, SEV_INFO, 'PROTECTED_INHERITANCE', + "Protected inheritance is rarely needed; did you mean public?"); + } + + # Check for private inheritance (composition might be better) + if ($line =~ /\bclass\s+\w+\s*:\s*(?:private\s+)?(\w+)/ && $line !~ /public|protected/) { + my $base = $1; + next if $base =~ /^(?:public|protected|private)$/; + report($filepath, $linenum, SEV_INFO, 'PRIVATE_INHERITANCE', + "Private inheritance from '$base'; consider composition (has-a) instead of inheritance (is-a)"); + } + } +} + +# =========================================================================== +# Additional Checks: Type Conversion Safety +# =========================================================================== + +sub check_type_conversions { + my ($filepath, $lines_ref, $changed_ref) = @_; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + next unless exists $changed_ref->{$linenum}; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Check for reinterpret_cast usage + if ($line =~ /\breinterpret_cast\b/) { + report($filepath, $linenum, SEV_WARNING, 'REINTERPRET_CAST', + "reinterpret_cast is dangerous; ensure this cast is necessary and documented"); + } + + # Check for const_cast usage (often a code smell) + if ($line =~ /\bconst_cast\b/) { + report($filepath, $linenum, SEV_INFO, 'CONST_CAST', + "const_cast may indicate a design issue; consider if the const can be removed upstream"); + } + + # Check for dynamic_cast without null check + if ($line =~ /dynamic_cast<[^>]+\*>\s*\((\w+)\)/) { + my $var = $1; + # Check if result is null-checked + unless ($line =~ /if\s*\(/ || $line =~ /assert|Q_ASSERT/) { + if ($i + 1 < scalar @$lines_ref) { + my $next = $lines_ref->[$i + 1]; + unless ($next =~ /if\s*\(\s*\w+\s*(?:!=\s*nullptr|==\s*nullptr|\)|!)/) { + report($filepath, $linenum, SEV_WARNING, 'DYNAMIC_CAST_NO_CHECK', + "dynamic_cast result should be null-checked"); + } + } + } + } + + # Check for narrowing conversions in initialization + if ($line =~ /\bint\s+\w+\s*=\s*\w+\.(?:size|count|length)\s*\(/) { + report($filepath, $linenum, SEV_INFO, 'NARROWING_CONVERSION', + "Potential narrowing conversion from size_t/qsizetype to int"); + } + + # Check for implicit bool conversion from pointer + if ($line =~ /\bif\s*\(\s*!?\s*\w+\.get\s*\(\s*\)\s*\)/) { + # This is actually fine in C++ but some prefer explicit nullptr check + } + } +} + +# =========================================================================== +# Additional Checks: Documentation and Readability +# =========================================================================== + +sub check_documentation { + my ($filepath, $lines_ref, $changed_ref, $ftype) = @_; + + return unless $ftype eq FTYPE_HEADER; + + my $in_class = 0; + my $class_name = ''; + my $public_method_count = 0; + my $documented_method_count = 0; + my $in_public = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + if ($line =~ /\bclass\s+(\w+)\b/ && $line !~ /;\s*$/) { + $in_class = 1; + $class_name = $1; + $public_method_count = 0; + $documented_method_count = 0; + } + + if ($in_class) { + if ($line =~ /^\s*public\s*:/) { + $in_public = 1; + } elsif ($line =~ /^\s*(?:protected|private)\s*:/) { + $in_public = 0; + } + + if ($in_public && $line =~ /^\s*(?:virtual\s+)?(?:static\s+)?(?:\w[\w:*&<> ]*\s+)?(\w+)\s*\(/ && $line !~ /^\s*(?:if|for|while|switch|return)/) { + my $method = $1; + next if $method eq $class_name; # Constructor + next if $method =~ /^~/; # Destructor + next if $method =~ /^operator/; # Operator + + $public_method_count++; + + # Check if the method has documentation (comment on previous lines) + if ($i > 0) { + my $prev = $lines_ref->[$i - 1]; + if ($prev =~ /\/\/\/|\/\*\*|\*\/|^\s*\*/) { + $documented_method_count++; + } + } + } + + if ($line =~ /^};/) { + $in_class = 0; + $in_public = 0; + + # Check documentation ratio for public API + if ($public_method_count > 5 && $documented_method_count == 0) { + report($filepath, 1, SEV_INFO, 'NO_API_DOCS', + "Class '$class_name' has $public_method_count public methods with no documentation"); + } + } + } + } +} + +# =========================================================================== +# Additional Checks: Complexity Metrics +# =========================================================================== + +sub check_cyclomatic_complexity { + my ($filepath, $lines_ref) = @_; + + my $in_function = 0; + my $function_name = ''; + my $function_start = 0; + my $brace_depth = 0; + my $complexity = 1; # Start at 1 + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + + # Skip comments + next if $line =~ /^\s*\/\//; + next if $line =~ /^\s*\*/; + + # Detect function start + if (!$in_function && $line =~ /^(?:\w[\w:*&<> ,]*\s+)?(\w+(?:::\w+)?)\s*\([^;]*$/) { + $function_name = $1; + $function_start = $i + 1; + next if $function_name =~ /^(?:if|for|while|switch|catch|return|class|struct|namespace|enum)$/; + + if ($line =~ /{/ || ($i + 1 < scalar @$lines_ref && $lines_ref->[$i + 1] =~ /^\s*{/)) { + $in_function = 1; + $brace_depth = 0; + $complexity = 1; + } + } + + if ($in_function) { + my $stripped = $line; + $stripped =~ s/"(?:[^"\\]|\\.)*"//g; + + $brace_depth += () = $stripped =~ /{/g; + $brace_depth -= () = $stripped =~ /}/g; + + # Count decision points + $complexity++ if $stripped =~ /\bif\s*\(/; + $complexity++ if $stripped =~ /\belse\s+if\b/; + $complexity++ if $stripped =~ /\bfor\s*\(/; + $complexity++ if $stripped =~ /\bwhile\s*\(/; + $complexity++ if $stripped =~ /\bcase\s+/; + $complexity++ if $stripped =~ /\bcatch\s*\(/; + $complexity++ if $stripped =~ /\?\s*[^:]/; # Ternary + $complexity++ if $stripped =~ /&&/; + $complexity++ if $stripped =~ /\|\|/; + + if ($brace_depth <= 0 && $stripped =~ /}/) { + if ($complexity > 40) { + report($filepath, $function_start, SEV_WARNING, 'HIGH_COMPLEXITY', + "Function '$function_name' has cyclomatic complexity of $complexity (recommended max: 40). Consider refactoring."); + } elsif ($complexity > 30) { + report($filepath, $function_start, SEV_INFO, 'MODERATE_COMPLEXITY', + "Function '$function_name' has cyclomatic complexity of $complexity (consider simplifying)"); + } + + $in_function = 0; + $complexity = 1; + } + } + } +} + +# =========================================================================== +# Additional Checks: Preprocessor Usage +# =========================================================================== + +sub check_preprocessor_usage { + my ($filepath, $lines_ref, $changed_ref) = @_; + + my $ifdef_depth = 0; + my $max_ifdef_depth = 0; + + for (my $i = 0; $i < scalar @$lines_ref; $i++) { + my $line = $lines_ref->[$i]; + my $linenum = $i + 1; + + # Track #ifdef depth + if ($line =~ /^\s*#\s*(?:if|ifdef|ifndef)\b/) { + $ifdef_depth++; + if ($ifdef_depth > $max_ifdef_depth) { + $max_ifdef_depth = $ifdef_depth; + } + } + if ($line =~ /^\s*#\s*endif\b/) { + $ifdef_depth--; + } + + next unless exists $changed_ref->{$linenum}; + + # Check for deeply nested preprocessor conditionals + if ($ifdef_depth > 3 && $line =~ /^\s*#\s*(?:if|ifdef|ifndef)\b/) { + report($filepath, $linenum, SEV_WARNING, 'DEEP_IFDEF', + "Preprocessor conditional nesting depth $ifdef_depth; consider refactoring"); + } + + # Check for #pragma warning disable without re-enable + if ($line =~ /^\s*#\s*pragma\s+warning\s*\(\s*disable/) { + # Check for corresponding enable + my $has_enable = 0; + for (my $j = $i + 1; $j < scalar @$lines_ref; $j++) { + if ($lines_ref->[$j] =~ /^\s*#\s*pragma\s+warning\s*\(\s*(?:default|enable)/) { + $has_enable = 1; + last; + } + } + unless ($has_enable) { + report($filepath, $linenum, SEV_WARNING, 'PRAGMA_NO_RESTORE', + "#pragma warning disable without corresponding restore"); + } + } + + # Check for #define with complex expressions (should be constexpr) + if ($line =~ /^\s*#\s*define\s+(\w+)\s+\(.*[+\-*\/].*\)/) { + my $macro = $1; + next if $macro =~ /^_/; # System macros + report($filepath, $linenum, SEV_INFO, 'DEFINE_VS_CONSTEXPR', + "Complex #define expression; consider constexpr for type safety"); + } + + # Check for multi-line macros (hard to debug) + if ($line =~ /^\s*#\s*define\b.*\\$/) { + my $macro_lines = 1; + for (my $j = $i + 1; $j < scalar @$lines_ref; $j++) { + $macro_lines++; + last unless $lines_ref->[$j] =~ /\\$/; + } + if ($macro_lines > 10) { + report($filepath, $linenum, SEV_WARNING, 'LARGE_MACRO', + "Multi-line macro ($macro_lines lines); consider using inline function or template"); + } + } + } +} + +# =========================================================================== +# Wire up additional checks in check_cpp_conventions +# =========================================================================== + +# Override the original check_cpp_conventions to include new checks +{ + no warnings 'redefine'; + my $original_check_cpp = \&check_cpp_conventions; + + *check_cpp_conventions = sub { + my ($filepath, $lines_ref, $changed_ref, $ftype) = @_; + + # Call original checks + $original_check_cpp->($filepath, $lines_ref, $changed_ref, $ftype); + + # Additional checks + check_template_style($filepath, $lines_ref, $changed_ref); + check_raii_patterns($filepath, $lines_ref, $changed_ref); + check_move_semantics($filepath, $lines_ref, $changed_ref); + check_concurrency_patterns($filepath, $lines_ref, $changed_ref); + check_operator_overloading($filepath, $lines_ref, $changed_ref); + check_file_organization($filepath, $lines_ref, $changed_ref, $ftype); + check_signal_slot_details($filepath, $lines_ref, $changed_ref); + check_json_patterns($filepath, $lines_ref, $changed_ref); + check_network_patterns($filepath, $lines_ref, $changed_ref); + check_logging_patterns($filepath, $lines_ref, $changed_ref); + check_path_handling($filepath, $lines_ref, $changed_ref); + check_error_handling($filepath, $lines_ref, $changed_ref); + check_inheritance_patterns($filepath, $lines_ref, $changed_ref); + check_type_conversions($filepath, $lines_ref, $changed_ref); + check_documentation($filepath, $lines_ref, $changed_ref, $ftype); + check_cyclomatic_complexity($filepath, $lines_ref); + check_preprocessor_usage($filepath, $lines_ref, $changed_ref); + }; +} + +# =========================================================================== +# Additional Rule Documentation +# =========================================================================== + +$RULE_DOCS{'TEMPLATE_BLANK_LINE'} = 'Blank line between template<> and declaration'; +$RULE_DOCS{'COMPLEX_TEMPLATE'} = 'Template with many parameters'; +$RULE_DOCS{'C_FILE_IO'} = 'C-style file I/O (use Qt/C++ alternatives)'; +$RULE_DOCS{'MANUAL_LOCK'} = 'Manual mutex lock (use RAII guards)'; +$RULE_DOCS{'RAW_HANDLE'} = 'Raw OS handle (use wrapper classes)'; +$RULE_DOCS{'GOTO_CLEANUP'} = 'Goto cleanup pattern (use RAII)'; +$RULE_DOCS{'MOVE_CONST'} = 'std::move on const object (no effect)'; +$RULE_DOCS{'USE_AFTER_MOVE'} = 'Use of moved-from object'; +$RULE_DOCS{'RETURN_STD_MOVE'} = 'return std::move prevents RVO'; +$RULE_DOCS{'PUSH_BACK_TEMP'} = 'push_back with temporary (use emplace_back)'; +$RULE_DOCS{'VOLATILE_SYNC'} = 'volatile is not synchronization'; +$RULE_DOCS{'SLEEP_SYNC'} = 'Sleep-based waiting'; +$RULE_DOCS{'THREAD_UNSAFE_SINGLETON'} = 'Thread-unsafe singleton pattern'; +$RULE_DOCS{'STATIC_IN_THREAD'} = 'Static variable in threaded context'; +$RULE_DOCS{'OPERATOR_CONST'} = 'Operator missing const version'; +$RULE_DOCS{'MISSING_OPERATOR_NEQ'} = 'operator== without operator!='; +$RULE_DOCS{'OPERATOR_SELF_ASSIGN'} = 'Copy assignment without self-check'; +$RULE_DOCS{'FILE_CLASS_MISMATCH'} = 'File name doesn\'t match class name'; +$RULE_DOCS{'FILE_NAMING'} = 'File naming convention (PascalCase)'; +$RULE_DOCS{'SIGNAL_NAMING'} = 'Signal naming convention'; +$RULE_DOCS{'CONNECT_LIFETIME'} = 'Lambda captures this in connect()'; +$RULE_DOCS{'JSON_NO_ERROR_CHECK'} = 'JSON parsing without error check'; +$RULE_DOCS{'JSON_UNCHECKED_ACCESS'} = 'Unchecked JSON value access'; +$RULE_DOCS{'HTTP_NOT_HTTPS'} = 'Insecure HTTP URL'; +$RULE_DOCS{'HARDCODED_URL'} = 'Hardcoded URL'; +$RULE_DOCS{'NETWORK_NO_ERROR'} = 'Network request without error handling'; +$RULE_DOCS{'DEBUG_FOR_ERROR'} = 'qDebug for error message'; +$RULE_DOCS{'CRITICAL_FOR_INFO'} = 'qCritical for informational message'; +$RULE_DOCS{'QDEBUG_ENDL'} = 'Unnecessary endl in qDebug'; +$RULE_DOCS{'TERSE_LOG_MESSAGE'} = 'Very short log message'; +$RULE_DOCS{'PATH_CONCATENATION'} = 'String concatenation for paths'; +$RULE_DOCS{'HARDCODED_BACKSLASH'} = 'Hardcoded backslash in path'; +$RULE_DOCS{'DIRECT_FS_ACCESS'} = 'Direct filesystem access vs FS:: helpers'; +$RULE_DOCS{'IGNORED_RETURN'} = 'Ignored return value of important function'; +$RULE_DOCS{'OPEN_NO_CHECK'} = 'File open without error check'; +$RULE_DOCS{'UNTRANSLATED_ERROR'} = 'Untranslated error message'; +$RULE_DOCS{'DEEP_INHERITANCE'} = 'Deep inheritance hierarchy'; +$RULE_DOCS{'PROTECTED_INHERITANCE'} = 'Protected inheritance (unusual)'; +$RULE_DOCS{'PRIVATE_INHERITANCE'} = 'Private inheritance (consider composition)'; +$RULE_DOCS{'REINTERPRET_CAST'} = 'Dangerous reinterpret_cast'; +$RULE_DOCS{'CONST_CAST'} = 'const_cast usage (design smell)'; +$RULE_DOCS{'DYNAMIC_CAST_NO_CHECK'} = 'dynamic_cast without null check'; +$RULE_DOCS{'NARROWING_CONVERSION'} = 'Potential narrowing conversion'; +$RULE_DOCS{'NO_API_DOCS'} = 'No documentation for public API'; +$RULE_DOCS{'HIGH_COMPLEXITY'} = 'High cyclomatic complexity'; +$RULE_DOCS{'MODERATE_COMPLEXITY'} = 'Moderate cyclomatic complexity'; +$RULE_DOCS{'DEEP_IFDEF'} = 'Deep preprocessor conditional nesting'; +$RULE_DOCS{'PRAGMA_NO_RESTORE'} = 'Pragma warning without restore'; +$RULE_DOCS{'DEFINE_VS_CONSTEXPR'} = 'Complex #define (use constexpr)'; +$RULE_DOCS{'LARGE_MACRO'} = 'Large multi-line macro'; + +# =========================================================================== +# Run +# =========================================================================== + +# Check for --self-test before normal option parsing +if (grep { $_ eq '--self-test' } @ARGV) { + @ARGV = grep { $_ ne '--self-test' } @ARGV; + $opt_color = (-t STDOUT) ? 1 : 0; + exit(run_self_test() ? 0 : 1); +} + +# Check for --list-rules +if (grep { $_ eq '--list-rules' } @ARGV) { + $opt_color = (-t STDOUT) ? 1 : 0; + list_rules(); + exit(0); +} + +main(); + +__END__ + +=head1 NAME + +checkpatch.pl - MeshMC Coding Style and Convention Checker + +=head1 SYNOPSIS + + checkpatch.pl [OPTIONS] [FILES...] + git diff | checkpatch.pl --diff + checkpatch.pl --git HEAD~1 + checkpatch.pl --dir launcher/ + checkpatch.pl --repository --summary + checkpatch.pl --self-test + checkpatch.pl --list-rules + +=head1 DESCRIPTION + +This script checks C++, header, and CMake source files in the MeshMC project +for adherence to the project's coding conventions. It is inspired by the Linux +kernel's checkpatch.pl but tailored specifically for MeshMC's Qt/C++ codebase. + +The script supports multiple input modes: checking individual files, entire +directories recursively, unified diffs from stdin, or git diffs from a specific +reference. It can also attempt to fix simple issues like trailing whitespace +and CRLF line endings. + +=head1 CONVENTIONS ENFORCED + +=head2 C++ Style + +=over 4 + +=item * 4-space indentation (no tabs allowed) + +=item * K&R brace placement (opening brace on same line for control structures) + +=item * PascalCase for class and struct names + +=item * camelCase for method and function names + +=item * m_ prefix for member variables (m_camelCase) + +=item * UPPER_SNAKE_CASE for preprocessor macros + +=item * #pragma once for header guards (no #ifndef guards) + +=item * SPDX license headers at top of every file + +=item * Line length maximum of 120 characters (configurable) + +=item * Pointer/reference symbols adjacent to variable name (Type *var, Type &var) + +=item * Modern C++ features: nullptr over NULL, enum class over enum, using over typedef + +=item * Smart pointers (make_shared/make_unique) instead of raw new/delete + +=item * C++ headers (cstdio, cstring) instead of C headers (stdio.h, string.h) + +=item * No std::string - use QString throughout + +=item * No std::cout/cerr - use qDebug()/qWarning()/qCritical() + +=item * Virtual destructors for polymorphic base classes + +=item * override keyword for virtual method overrides in derived classes + +=item * explicit keyword for single-parameter constructors + +=back + +=head2 Qt Conventions + +=over 4 + +=item * Q_OBJECT macro required in all QObject-derived classes + +=item * Modern Qt6 connect() syntax preferred over SIGNAL/SLOT macros + +=item * signals: and slots: keywords preferred over Q_SIGNALS/Q_SLOTS + +=item * Signal names should use past-tense or progressive naming (e.g., changed, loading) + +=item * Slot names following on_widgetName_signalName pattern for auto-connections + +=item * QProcess for process execution instead of system() + +=item * tr() for user-visible strings for localization + +=item * FS::PathCombine() for path construction instead of string concatenation + +=item * Json::requireDocument() helpers for JSON parsing with error handling + +=item * shared_qobject_ptr and unique_qobject_ptr for Qt object ownership + +=back + +=head2 CMake Conventions + +=over 4 + +=item * 3-space indentation (no tabs) + +=item * Lowercase function names (set(), add_executable(), etc.) + +=item * UPPER_SNAKE_CASE for variable names (CORE_SOURCES, CMAKE_CXX_STANDARD) + +=item * Explicit source file listing (no file(GLOB)) + +=item * target_* commands preferred over global add_* commands + +=item * SPDX license headers in comments + +=back + +=head2 File Organization + +=over 4 + +=item * PascalCase file names matching primary class name + +=item * .h extension for headers, .cpp for implementation + +=item * Test files named *_test.cpp using Qt Test framework + +=item * Include ordering: local includes, then Qt includes, then STL includes + +=back + +=head1 SEVERITY LEVELS + +The script uses four severity levels: + +=over 4 + +=item B<ERROR> (exit code 1) + +Must fix before merging. Includes missing header guards, CRLF line endings, +dangerous functions (gets, sprintf), using namespace in headers, throw by +pointer, missing Q_OBJECT macro, and hardcoded credentials. + +=item B<WARNING> (exit code 2) + +Should fix. Includes trailing whitespace, old Qt connect syntax, NULL instead +of nullptr, C-style casts, security-sensitive functions, ignored return values, +commented-out code, and goto cleanup patterns. + +=item B<INFO> (shown with --verbose) + +Style suggestions. Includes include ordering, const reference suggestions, +prefer enum class, magic numbers, documentation coverage, and complexity +metrics. + +=item B<STYLE> (shown with --verbose) + +Minor cosmetic preferences. Includes pointer alignment, keyword spacing, +typedef vs using, and Yoda conditions. + +=back + +=head1 OPTIONS + +=over 4 + +=item B<--diff> + +Read unified diff from stdin. Only changed lines (added lines in diff context) +are checked for line-level rules. File-level rules (header guard, license) are +still applied to the full file if accessible. + +=item B<--git> I<ref> + +Check changes since the specified git ref. Runs C<git diff ref> internally. +The ref is validated to prevent shell injection. Supports any valid git ref +format (HEAD~N, branch names, commit hashes, tags). + +=item B<--file> I<path> + +Check a specific file. Can be repeated to check multiple files. All lines +in the file are checked. + +=item B<--dir> I<path> + +Check all source files (.cpp, .h, .hpp, CMakeLists.txt, .cmake) in the +specified directory recursively. Automatically excludes build/, libraries/, +.git/, and auto-generated files. + +=item B<--repository>, B<--repo>, B<-R> + +Scan the entire git repository. The script finds the repository root +by running C<git rev-parse --show-toplevel> and checks all source files +starting from that root. Automatically excludes build/, libraries/, +.git/, and auto-generated files. Equivalent to running +C<--dir E<lt>repo-rootE<gt>> but without needing to know the root path. +Fails with exit code 3 if no git repository is found. + +=item B<--fix> + +Attempt to fix simple issues in-place. Currently fixes: trailing whitespace +removal, CRLF to LF conversion. Use with caution and review changes. + +=item B<--quiet, -q> + +Only show ERROR-level issues. Suppresses warnings, info, and style messages. + +=item B<--verbose, -v> + +Show all issues including INFO and STYLE levels. Also prints file names as +they are being checked. + +=item B<--summary, -s> + +Print a summary table at the end showing total files checked, lines checked, +and counts of errors, warnings, and info messages. + +=item B<--color> + +Force colored output regardless of terminal detection. + +=item B<--no-color> + +Disable colored output even in a terminal. + +=item B<--max-line-length> I<n> + +Set maximum line length. Default is 120 characters. Lines exceeding this +length generate warnings. Lines exceeding length + 20 generate errors. + +=item B<--exclude> I<pattern> + +Exclude files matching the specified glob pattern. Can be repeated. +Patterns are matched against the full file path. Default exclusions +include build/, libraries/, and auto-generated files. + +=item B<--self-test> + +Run internal self-tests to verify the script is working correctly. +Exits with code 0 if all tests pass, 1 otherwise. + +=item B<--list-rules> + +Print a categorized list of all rules with their descriptions. + +=item B<--help, -h> + +Show usage information. + +=item B<--version, -V> + +Show version information. + +=back + +=head1 EXIT CODES + +=over 4 + +=item B<0> - No issues found (clean) + +=item B<1> - One or more ERROR-level issues found + +=item B<2> - WARNING-level issues found (no errors) + +=item B<3> - Script usage error (invalid options) + +=back + +=head1 INTEGRATION + +=head2 Git Pre-Commit Hook + +Add to your lefthook.yml: + + pre-commit: + jobs: + - name: checkpatch + run: | + git diff --cached --diff-filter=ACMR | ./scripts/checkpatch.pl --diff + +=head2 CI Pipeline + + checkpatch: + script: + - ./scripts/checkpatch.pl --git origin/main --summary -q + +=head2 Editor Integration + +Most editors can run external linters. Configure checkpatch.pl as an +external tool with the --file flag pointing to the current file. + +=head1 EXAMPLES + + # Check all source files in launcher/ + ./scripts/checkpatch.pl --dir launcher/ --summary + + # Scan entire repository + ./scripts/checkpatch.pl --repository --summary + + # Scan entire repository, errors only + ./scripts/checkpatch.pl -R -q --summary + + # Check a specific file + ./scripts/checkpatch.pl --file launcher/Application.cpp + + # Check recent git changes + ./scripts/checkpatch.pl --git HEAD~3 --summary + + # Check staged changes before commit + git diff --cached | ./scripts/checkpatch.pl --diff + + # Check changes against main branch + git diff main | ./scripts/checkpatch.pl --diff --summary + + # Fix trivial issues in a file + ./scripts/checkpatch.pl --file foo.cpp --fix + + # Quiet mode - errors only + ./scripts/checkpatch.pl --dir launcher/ -q + + # Verbose mode - all details + ./scripts/checkpatch.pl --dir launcher/ -v --summary + + # Custom line length + ./scripts/checkpatch.pl --file foo.cpp --max-line-length 100 + + # Exclude test files + ./scripts/checkpatch.pl --dir launcher/ --exclude '*_test.cpp' + + # List all available rules + ./scripts/checkpatch.pl --list-rules + + # Run self-tests + ./scripts/checkpatch.pl --self-test + +=head1 RULE CATEGORIES + +Rules are organized into the following categories: + +=over 4 + +=item B<Common> - Whitespace, line endings, blank lines + +=item B<License> - SPDX headers, copyright notices + +=item B<Header Guards> - #pragma once enforcement + +=item B<Indentation> - Tab/space checking, indent width + +=item B<Line Length> - Maximum line length enforcement + +=item B<Naming> - Class, method, variable, macro naming conventions + +=item B<Brace Style> - K&R brace placement + +=item B<Pointer/Reference> - Alignment conventions + +=item B<Spacing> - Keyword spacing, comma spacing, brace spacing + +=item B<Qt> - Q_OBJECT, connect syntax, signal/slot conventions + +=item B<Includes> - Include ordering, C vs C++ headers, path style + +=item B<Comments> - Comment style, commented-out code, TODO format + +=item B<Control Flow> - Braces, goto, ternary, Yoda conditions + +=item B<Strings> - QString vs std::string, logging functions + +=item B<Memory> - Smart pointers, raw new/delete, C memory functions + +=item B<Const> - Const reference parameters + +=item B<Enum> - enum class preference + +=item B<Constructor> - explicit keyword, initializer lists + +=item B<Function> - Length limits, nesting depth + +=item B<Deprecated> - NULL, C casts, typedef, auto_ptr + +=item B<Security> - Dangerous functions, hardcoded credentials, HTTP + +=item B<Tests> - QTest framework conventions + +=item B<Exceptions> - Throw/catch conventions + +=item B<Virtual/Override> - Virtual destructors, override keyword + +=item B<Auto> - auto keyword usage + +=item B<Lambda> - Capture lists, lambda length + +=item B<Switch> - Fall-through, default case + +=item B<Class> - Access specifier ordering + +=item B<Debug> - Debug print cleanup + +=item B<Style> - Multiple statements, magic numbers, using namespace + +=item B<CMake> - CMake-specific conventions + +=item B<Templates> - Template parameter style + +=item B<RAII> - Resource management patterns + +=item B<Move Semantics> - std::move usage + +=item B<Concurrency> - Thread safety patterns + +=item B<Operators> - Operator overloading conventions + +=item B<File Organization> - File/class name matching + +=item B<JSON> - JSON parsing patterns + +=item B<Network> - URL and network patterns + +=item B<Logging> - Log level consistency + +=item B<Paths> - Path handling conventions + +=item B<Error Handling> - Return value checking + +=item B<Inheritance> - Inheritance patterns + +=item B<Type Conversions> - Cast safety + +=item B<Documentation> - API documentation coverage + +=item B<Complexity> - Cyclomatic complexity metrics + +=item B<Preprocessor> - Macro and #ifdef patterns + +=back + +=head1 KNOWN LIMITATIONS + +=over 4 + +=item * Does not parse C++ fully; uses regex heuristics which may produce +false positives or miss some patterns. + +=item * String literal detection is approximate; multiline raw strings +may not be handled perfectly. + +=item * Template metaprogramming patterns may trigger false warnings. + +=item * Alignment-based indentation (e.g., aligning function parameters) +is allowed but may trigger info-level messages. + +=item * The --fix mode only handles trailing whitespace and CRLF conversion; +it does not fix naming, indentation, or structural issues. + +=back + +=head1 AUTHOR + +MeshMC Project - Project Tick + +=head1 LICENSE + +GPL-3.0-or-later + +=cut |
