summaryrefslogtreecommitdiff
path: root/meshmc/scripts/checkpatch.pl
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/scripts/checkpatch.pl
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-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-xmeshmc/scripts/checkpatch.pl5480
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/&/&amp;/g;
+ $str =~ s/</&lt;/g;
+ $str =~ s/>/&gt;/g;
+ $str =~ s/"/&quot;/g;
+ $str =~ s/'/&#39;/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