CI: Refactor C++ checks linting setup.

This commit is contained in:
Joao Matos
2025-03-05 22:41:42 +00:00
committed by Chris Hennes
parent 17719536bc
commit 37d11fcfbe
9 changed files with 844 additions and 387 deletions

View File

@@ -76,81 +76,6 @@ on:
default: true
type: boolean
required: false
cpplintFilters:
default: >-
'
+build/deprecated,
-build/endif_comment,
+build/explicit_make_pair,
-build/forward_decl,
-build/header_guard,
+build/include,
-build/include_subdir,
-build/include_alpha,
-build/include_order,
+build/include_what_you_use,
+build/namespaces_headers,
+build/namespaces_literals,
-build/namespaces,
+build/printf_format,
+build/storage_class,
-legal/copyright,
+readability/alt_tokens,
-readability/braces,
+readability/casting,
+readability/check,
+readability/constructors,
+readability/fn_size,
+readability/inheritance,
+readability/multiline_comment,
+readability/multiline_string,
+readability/namespace,
-readability/nolint,
+readability/nul,
+readability/strings,
-readability/todo,
+readability/utf8,
+runtime/arrays,
+runtime/casting,
+runtime/explicit,
+runtime/int,
+runtime/init,
+runtime/invalid_increment,
+runtime/member_string_references,
+runtime/memset,
+runtime/operator,
+runtime/printf,
+runtime/printf_format,
+runtime/references,
+runtime/string,
+runtime/threadsafe_fn,
+runtime/vlog,
-whitespace/blank_line,
-whitespace/braces,
-whitespace/comma,
-whitespace/comments,
-whitespace/empty_conditional_body,
-whitespace/empty_if_body,
-whitespace/empty_loop_body,
-whitespace/end_of_line,
-whitespace/ending_newline,
-whitespace/forcolon,
-whitespace/indent,
-whitespace/indent_namespace,
-whitespace/line_length,
-whitespace/newline,
-whitespace/operators,
-whitespace/parens,
-whitespace/semicolon,
-whitespace/tab,
-whitespace/todo
'
type: string
required: false
cpplintLineLength:
default: 120
type: string
required: false
cpplintFailSilent:
default: true
type: boolean
@@ -325,352 +250,89 @@ jobs:
--files "${{ inputs.changedPythonFiles }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
# C++ linting steps
- name: Install FreeCAD dependencies
if: inputs.changedCppFiles != '' && always()
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
doxygen \
graphviz \
imagemagick \
libboost-date-time-dev \
libboost-dev \
libboost-filesystem-dev \
libboost-graph-dev \
libboost-iostreams-dev \
libboost-program-options-dev \
libboost-python-dev \
libboost-regex-dev \
libboost-serialization-dev \
libboost-thread-dev \
libcoin-dev \
libeigen3-dev \
libkdtree++-dev \
libmedc-dev \
libocct-data-exchange-dev \
libocct-ocaf-dev \
libocct-visualization-dev \
libopencv-dev \
libproj-dev \
libpyside2-dev \
libqt5opengl5-dev \
libqt5svg5-dev \
libqt5x11extras5-dev \
libshiboken2-dev \
libspnav-dev \
libvtk9-dev \
libx11-dev \
libxerces-c-dev \
libyaml-cpp-dev \
libzipios++-dev \
netgen \
netgen-headers \
occt-draw \
pyside2-tools \
python3-dev \
python3-git \
python3-markdown \
python3-matplotlib \
python3-packaging \
python3-pivy \
python3-ply \
python3-pyside2.qtcore \
python3-pyside2.qtgui \
python3-pyside2.qtnetwork \
python3-pyside2.qtsvg \
python3-pyside2.qtwidgets \
qtbase5-dev \
qttools5-dev \
shiboken2 \
swig \
ccache \
xvfb
run: ./package/ubuntu/install-apt-packages.sh
- name: Run CMake # This is needed for Clang tools to work correctly
if: inputs.changedCppFiles != '' && always()
run: |
mkdir build && cmake -S ./ -B ./build/ -D CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE
- name: Check old Qt string-based connections (https://wiki.qt.io/New_Signal_Slot_Syntax)
if: inputs.checkQtConnections && inputs.changedCppFiles != '' && always()
continue-on-error: ${{ inputs.qtConnectionsFailSilent }}
run: |
qtconnectionSyntax=0
exclude="*[.md,.log,.ts,.git]"
# Check all files for QT string-based connections
for file in ${{ inputs.changedFiles }} #TODO does this makes sense in Python files ?
do
grep -nIHE --exclude="$exclude" $' SIGNAL| SLOT' $file | sed -e "s/$/ <--Consider using Functor-Based Connections/" >> ${{ env.logdir }}qtConnections.log || true #TODO seems to trigger false positives
done
# Write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}qtConnections.log ]; then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/grepMatcherWarning.json"
cat ${{ env.logdir }}qtConnections.log
echo "::remove-matcher owner=grepMatcher-warning::"
qtconnectionSyntax=$(wc -l < ${{ env.logdir }}qtConnections.log)
fi
echo "Found $qtconnectionSyntax QT string-based connections"
# Write the report
if [ $qtconnectionSyntax -gt 0 ]; then
echo "<details><summary>:information_source: Found $qtconnectionSyntax QT string-based connections :arrow_right: consider using QT functor-Based Connections</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# documentation link
echo "For more information see: https://wiki.qt.io/New_Signal_Slot_Syntax or https://github.com/FreeCAD/FreeCAD/issues/6166" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}qtConnections.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo ":heavy_check_mark: No string-based connections found " >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $qtconnectionSyntax -eq 0 ]
python3 tools/lint/qt_connections.py \
--files "${{ inputs.changedFiles }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Cpplint
if: inputs.checkCpplint && inputs.changedCppFiles != '' && always()
continue-on-error: ${{ inputs.cpplintFailSilent }}
run: |
cpplintErrors=0
pip install --break-system-packages cpplint
# Run cpplint
for file in ${{ inputs.changedCppFiles }}
do
cpplint --filter=${{ inputs.cpplintFilters }} --linelength=${{ inputs.cpplintLineLength }} $file &>> ${{ env.logdir }}cpplint.log || true
done
# If cpplint has run successfully, write the Log to the console with the Problem Matchers
if [ ${{ env.logdir }}cpplint.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/cpplint.json"
cat ${{ env.logdir }}cpplint.log
echo "::remove-matcher owner=cpplint::"
cpplintErrors=$(grep -nIHE "\[[0-9]\]$" ${{ env.logdir }}cpplint.log | wc -l ) || true
fi
echo "Found $cpplintErrors cpplint errors"
# Write the report
if [ $cpplintErrors -gt 0 ]
then
echo "<details><summary>:warning: CppLint found $cpplintErrors errors / warnings</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: No cpplint errors found </summary> " >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}cpplint.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $cpplintErrors -eq 0 ]
python3 tools/lint/cpplint.py \
--files "${{ inputs.changedCppFiles }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Clang-format
if: inputs.checkClangFormat && inputs.changedCppFiles != '' && always()
continue-on-error: ${{ inputs.clangFormatFailSilent }}
run: |
clangFormatErrors=0
# Run clang-format on all cpp files
clang-format --dry-run --ferror-limit=1 --verbose --style=${{ inputs.clangStyle }} ${{ inputs.changedCppFiles }} &>> ${{ env.logdir }}clang-format.log || true
# If clang-format has run successfully, write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}clang-format.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/clang.json"
cat ${{ env.logdir }}clang-format.log
echo "::remove-matcher owner=clang::"
clangFormatErrors=$(grep -nIHE "\[-Wclang-format-violations]$" ${{ env.logdir }}clang-format.log | wc -l ) || true
fi
echo "Found $clangFormatErrors clang-format errors"
# Write the report
if [ $clangFormatErrors -gt 0 ]
then
echo "<details><summary>:pencil2: Clang-format would reformat $clangFormatErrors files</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: Clang-format would reformat no file</summary> " >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}clang-format.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $clangFormatErrors -eq 0 ]
python3 tools/lint/clang_format.py \
--files "${{ inputs.changedCppFiles }}" \
--clang-style "${{ inputs.clangStyle }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Codespell
if: inputs.checkSpelling && always()
continue-on-error: ${{ inputs.codespellFailSilent }}
run: |
pip install --break-system-packages codespell
wget https://raw.githubusercontent.com/codespell-project/codespell/master/codespell_lib/data/dictionary.txt
#wget https://raw.githubusercontent.com/codespell-project/codespell/master/codespell_lib/data/dictionary_rare.txt
set +e
misspellings=$( { codespell --quiet-level 3 --summary --count --ignore-words ${{ inputs.listIgnoredMisspelling }} --skip ${{ inputs.spellingIgnore }} -D dictionary.txt ${{ inputs.changedFiles }} > ${{ env.logdir }}codespell.log ; } 2>&1 )
set -e
# If codespell has run successfully, write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}codespell.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/codespell.json"
cat ${{ env.logdir }}codespell.log
echo "::remove-matcher owner=codespell::"
fi
echo "Found $misspellings misspellings"
# Write the report
if [ $misspellings -gt 0 ]
then
echo "<details><summary>:pencil2: Codespell found $misspellings misspellings</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: Codespell found no misspellings</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "To ignore false positives, append the word to the [.github/codespellignore](https://github.com/FreeCAD/FreeCAD/blob/master/.github/codespellignore) file (lowercase)" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}codespell.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $misspellings -eq 0 ]
python3 tools/lint/codespell.py \
--files "${{ inputs.changedFiles }}" \
--ignore-words "${{ inputs.listIgnoredMisspelling }}" \
--skip "${{ inputs.spellingIgnore }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Clang-tidy
if: inputs.checkClangTidy && inputs.changedCppFiles != '' && always()
continue-on-error: ${{ inputs.clangTidyFailSilent }}
run: |
clangTidyErrors=0
clangTidyWarnings=0
clangTidyNotes=0
sudo apt-get install -y --no-install-recommends clang-tidy
#TODO: check where this "clang-tidy.yaml" goes ; shall this be put in the fixes ?
clang-tidy --quiet --format-style=${{ inputs.clangStyle }} --export-fixes=clang-tidy.yaml -p build/ --explain-config &>> ${{ env.logdir }}clang-tidy-enabled-checks.log
# Run clang-tidy on all cpp files
set +e
clang-tidy --quiet --format-style=${{ inputs.clangStyle }} --export-fixes=clang-tidy.yaml -p build/ ${{ inputs.changedCppFiles }} &>> ${{ env.logdir }}clang-tidy.log
exitCode=$?
set -e
# If clang-tidy has run successfully, write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}clang-tidy.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/clang.json"
cat ${{ env.logdir }}clang-tidy.log
echo "::remove-matcher owner=clang::"
clangTidyErrors=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): error: .+$" ${{ env.logdir }}clang-tidy.log | wc -l ) || true
clangTidyWarnings=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): warning: .+$" ${{ env.logdir }}clang-tidy.log | wc -l ) || true
clangTidyNotes=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): note: .+$" ${{ env.logdir }}clang-tidy.log | wc -l ) || true
fi
echo "Found $clangTidyErrors errors, $clangTidyWarnings warnings, $clangTidyNotes notes"
# Write the report
if [ $clangTidyErrors -gt 0 ]
then
echo "<details><summary>:fire: Clang-Tidy found :fire: $clangTidyErrors errors, :warning: $clangTidyWarnings warnings and :pencil2: $clangTidyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ $clangTidyWarnings -gt 0 ]
then
echo "<details><summary>:warning: Clang-Tidy found :warning: $clangTidyWarnings warnings and :pencil2: $clangTidyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ $clangTidyNotes -gt 0 ]
then
echo "<details><summary>:pencil2: Clang-Tidy found :pencil2: $clangTidyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: Clang-Tidy found no errors, warnings or notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# List enabled checks in the report
echo "<details><summary>:information_source: Enabled checks</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}clang-tidy-enabled-checks.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}clang-tidy.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $clangTidyErrors -eq 0 ]
python3 tools/lint/clang_tidy.py \
--files "${{ inputs.changedCppFiles }}" \
--clang-style "${{ inputs.clangStyle }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Clazy
if: inputs.checkClazy && inputs.changedCppFiles != '' && always()
continue-on-error: ${{ inputs.clazyFailSilent }}
env:
CLAZY_HEADER_FILTER: "^$"
run: |
clazyErrors=0
clazyWarnings=0
clazyNotes=0
sudo apt-get install -y --no-install-recommends clazy
#TODO: check where this "clazy.yaml" goes ; shall this be put in the fixes ?
# Run clazy on all changed cpp files, ignoring any included headers
for file in ${{ inputs.changedCppFiles }}
do
clazy-standalone --export-fixes=clazy.yaml -checks=${{ inputs.clazyChecks }} -p build/compile_commands.json $file &>> ${{ env.logdir }}clazy.log || true
done
# If clazy has run successfully, write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}clazy.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/clang.json"
cat ${{ env.logdir }}clazy.log
echo "::remove-matcher owner=clang::"
clazyErrors=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): error: .+$" ${{ env.logdir }}clazy.log | wc -l ) || true
clazyWarnings=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): warning: .+$" ${{ env.logdir }}clazy.log | wc -l ) || true
clazyNotes=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): note: .+$" ${{ env.logdir }}clazy.log | wc -l ) || true
fi
echo "Found $clazyErrors errors, $clazyWarnings warnings, $clazyNotes notes"
# Write the report
if [ "$clazyErrors" -gt 0 ]
then
echo "<details><summary>:fire: Clazy found :fire: $clazyErrors errors, :warning: $clazyWarnings warnings and :pencil2: $clazyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ "$clazyWarnings" -gt 0 ]
then
echo "<details><summary>:warning: Clazy found :warning: $clazyWarnings warnings and :pencil2: $clazyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ "$clazyNotes" -gt 0 ]
then
echo "<details><summary>:pencil2: Clazy found :pencil2: $clazyNotes notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "[List of checks](https://github.com/KDE/clazy#list-of-checks), [This explains some of the clazy warnings](https://www.kdab.com/uncovering-32-qt-best-practices-compile-time-clazy/) " >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}clazy.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $clazyErrors -eq 0 ]
python3 tools/lint/clazy.py \
--files "${{ inputs.changedCppFiles }}" \
--clazy-checks "${{ inputs.clazyChecks }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
- name: Clazy-QT6
if: inputs.checkClazyQT6 && inputs.changedCppFiles != '' && github.ref == inputs.QT6Branch && always()
continue-on-error: ${{ inputs.clazyQT6FailSilent }}
run: |
clazyQT6Errors=0
clazyQT6Warnings=0
clazyQT6Notes=0
sudo apt-get install -y --no-install-recommends clazy
#TODO: check where this "clazyQT6.yaml" goes ; shall this be put in the fixes ?
# Run clazy checks for Qt6 on all cpp files
clazy-standalone --export-fixes=clazyQT6.yaml -checks=${{ inputs.clazyQT6Checks }} -p build/ ${{ inputs.changedCppFiles }} &>> ${{ env.logdir }}clazyQT6.log || true
# If clazy has run successfully, write the Log to the console with the Problem Matchers
if [ -f ${{ env.logdir }}clazyQT6.log ]
then
echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/clang.json"
cat ${{ env.logdir }}clazyQT6.log
echo "::remove-matcher owner=clang::"
clazyQT6Errors=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): error: .+$" ${{ env.logdir }}clazyQT6.log | wc -l ) || true
clazyQT6Warnings=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): warning: .+$" ${{ env.logdir }}clazyQT6.log | wc -l ) || true
clazyQT6Notes=$(grep -nIHE "^(.+):([0-9]+):([0-9]+): note: .+$" ${{ env.logdir }}clazyQT6.log | wc -l ) || true
fi
echo "Found $clazyQT6Errors errors, $clazyQT6Warnings warnings, $clazyQT6Notes notes"
# Write the report
if [ "$clazyQT6Errors" -gt 0 ]
then
echo "<details><summary>:fire: Clazy found :fire: $clazyQT6Errors errors, :warning: $clazyQT6Warnings warnings and :pencil2: $clazyQT6Notes notes for porting to QT6</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ "$clazyQT6Warnings" -gt 0 ]
then
echo "<details><summary>:warning: Clazy found :warning: $clazyQT6Warnings warnings and :pencil2: $clazyQT6Notes notes for porting to QT6</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
elif [ "$clazyNotes" -gt 0 ]
then
echo "<details><summary>:pencil2: Clazy found :pencil2: $clazyQT6Notes notes for porting to QT6</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
else
echo "<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes for porting to QT6</summary>" >> ${{env.reportdir}}${{ env.reportfilename }}
fi
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
cat ${{ env.logdir }}clazyQT6.log >> ${{env.reportdir}}${{ env.reportfilename }}
echo '```' >> ${{env.reportdir}}${{ env.reportfilename }}
echo "</details>" >> ${{env.reportdir}}${{ env.reportfilename }}
echo "" >> ${{env.reportdir}}${{ env.reportfilename }}
# Exit the step with appropriate code
[ $clazyQT6Errors -eq 0 ]
python3 tools/lint/clazy_qt6.py \
--files "${{ inputs.changedCppFiles }}" \
--clazy-qt6-checks "${{ inputs.clazyQT6Checks }}" \
--log-dir "${{ env.logdir }}" \
--report-file "${{ env.reportdir }}${{ env.reportfilename }}"
# Upload steps
- name: Upload logs and fixes
if: always()
uses: actions/upload-artifact@v4
@@ -679,6 +341,7 @@ jobs:
path: |
${{ env.logdir }}
${{ env.fixesdir }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
append_file,
emit_problem_matchers,
write_file,
add_common_arguments,
)
def count_clang_format_errors(log_text: str) -> int:
"""
Count lines that indicate clang-format violations.
The pattern looks for lines ending with "[-Wclang-format-violations]".
"""
pattern = r"\[-Wclang-format-violations\]$"
matches = re.findall(pattern, log_text, re.MULTILINE)
logging.debug("Matches found: %s", matches)
return len(matches)
def generate_markdown_report(clang_format_errors: int, output: str) -> str:
"""Generate a Markdown report section for the clang-format output."""
report_lines = []
if clang_format_errors > 0:
report_lines.append(
f"<details><summary>:pencil2: Clang-format would reformat"
f" {clang_format_errors} file(s)</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clang-format would reformat no file</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run clang-format in dry-run mode on C++ files and append a Markdown report."
)
add_common_arguments(parser)
parser.add_argument(
"--clang-style",
required=True,
help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).",
)
args = parser.parse_args()
init_environment(args)
cmd = (
f"clang-format --dry-run --ferror-limit=1 --verbose "
f"--style={args.clang_style} {args.files}"
)
stdout, stderr, _ = run_command(cmd)
output = stdout + "\n" + stderr
log_file = os.path.join(args.log_dir, "clang-format.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
clang_format_errors = count_clang_format_errors(output)
logging.info("Found %s clang-format errors", clang_format_errors)
report = generate_markdown_report(clang_format_errors, output)
append_file(args.report_file, report)
sys.exit(0 if clang_format_errors == 0 else 1)
if __name__ == "__main__":
main()

131
tools/lint/clang_tidy.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
add_common_arguments,
emit_problem_matchers,
write_file,
append_file,
)
def generate_markdown_report(
errors_count: int,
warnings_count: int,
notes_count: int,
clang_tidy_output: str,
enabled_checks_content: str,
) -> str:
"""Generate a Markdown report section for clang-tidy results and enabled checks."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clang-Tidy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clang-Tidy found :warning: {warnings_count} "
f"warnings and :pencil2: {notes_count} notes</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clang-Tidy found :pencil2: {notes_count} notes</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clang-Tidy found no errors, warnings or notes</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(clang_tidy_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
report_lines.append(
"<details><summary>:information_source: Enabled checks</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(enabled_checks_content)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def count_occurrences(pattern: str, text: str) -> int:
"""Count all occurrences of a regex pattern in text using MULTILINE mode."""
matches = re.findall(pattern, text, re.MULTILINE)
logging.info("Pattern '%s' found %d matches", pattern, len(matches))
return len(matches)
def main():
parser = argparse.ArgumentParser(
description="Run clang-tidy on provided C++ files and append a Markdown report."
)
add_common_arguments(parser)
parser.add_argument(
"--clang-style",
required=True,
help="Clang-format style (e.g., 'file' to use .clang-format or a specific style).",
)
args = parser.parse_args()
init_environment(args)
build_dir = "build"
clang_tidy_base_cmd = [
"clang-tidy",
"--quiet",
f"--format-style={args.clang_style}",
"--export-fixes=clang-tidy.yaml",
"-p",
build_dir,
]
enabled_cmd = clang_tidy_base_cmd + ["--explain-config"]
enabled_stdout, enabled_stderr, _ = run_command(enabled_cmd)
enabled_output = enabled_stdout + enabled_stderr
enabled_checks_log = os.path.join(args.log_dir, "clang-tidy-enabled-checks.log")
write_file(enabled_checks_log, enabled_output)
clang_cmd = clang_tidy_base_cmd + args.files.split()
clang_stdout, clang_stderr, _ = run_command(clang_cmd)
clang_tidy_output = clang_stdout + clang_stderr
clang_tidy_log = os.path.join(args.log_dir, "clang-tidy.log")
write_file(clang_tidy_log, clang_tidy_output)
emit_problem_matchers(clang_tidy_log, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = count_occurrences(error_pattern, clang_tidy_output)
warnings_count = count_occurrences(warning_pattern, clang_tidy_output)
notes_count = count_occurrences(note_pattern, clang_tidy_output)
logging.info(
"Found %d errors, %d warnings, %d notes",
errors_count,
warnings_count,
notes_count,
)
report = generate_markdown_report(
errors_count, warnings_count, notes_count, clang_tidy_output, enabled_output
)
append_file(args.report_file, report)
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

119
tools/lint/clazy.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def generate_markdown_report(
aggregated_output: str, errors_count: int, warnings_count: int, notes_count: int
) -> str:
"""Generate a Markdown report based on clazy output and issue counts."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clazy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clazy found :warning: {warnings_count} warnings "
f"and :pencil2: {notes_count} notes</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clazy found :pencil2: {notes_count} notes</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes</summary>"
)
report_lines.append("")
report_lines.append(
"[List of checks](https://github.com/KDE/clazy#list-of-checks), "
"[This explains some of the clazy warnings](https://www.kdab.com/uncovering-32-qt-best-practices-compile-time-clazy/)"
)
report_lines.append("````")
report_lines.append(aggregated_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Clazy on provided C++ files and append a Markdown report."
)
parser.add_argument(
"--files",
required=True,
help="A space-separated list (or glob-expanded string) of C++ files to check.",
)
parser.add_argument(
"--clazy-checks",
required=True,
help="Comma-separated list of clazy checks to run.",
)
parser.add_argument(
"--log-dir", required=True, help="Directory where the log file will be written."
)
parser.add_argument(
"--report-file",
required=True,
help="Path to the Markdown report file to append results.",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")
args = parser.parse_args()
init_environment(args)
build_compile_commands = "build/compile_commands.json"
fix_file = "clazy.yaml"
output = ""
file_list = args.files.split()
for file in file_list:
cmd = [
"clazy-standalone",
f"--export-fixes={fix_file}",
f"-checks={args.clazy_checks}",
"-p",
build_compile_commands,
file,
]
stdout, stderr, _ = run_command(cmd)
output += stdout + "\n" + stderr + "\n"
log_file = os.path.join(args.log_dir, "clazy.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = len(re.findall(error_pattern, output, re.MULTILINE))
warnings_count = len(re.findall(warning_pattern, output, re.MULTILINE))
notes_count = len(re.findall(note_pattern, output, re.MULTILINE))
logging.info(
f"Found {errors_count} errors, {warnings_count} warnings, {notes_count} notes"
)
report = generate_markdown_report(output, errors_count, warnings_count, notes_count)
append_file(args.report_file, report + "\n")
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

120
tools/lint/clazy_qt6.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def count_occurrences(pattern, text):
matches = re.findall(pattern, text, re.MULTILINE)
logging.debug(f"Found {len(matches)} matches for pattern: {pattern}")
return len(matches)
def generate_markdown_report(
output: str, errors_count: int, warnings_count: int, notes_count: int
) -> str:
"""Generate a Markdown report based on Clazy QT6 output and issue counts."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:fire: Clazy found :fire: {errors_count} errors, "
f":warning: {warnings_count} warnings and :pencil2: {notes_count} notes for porting to QT6</summary>"
)
elif warnings_count > 0:
report_lines.append(
f"<details><summary>:warning: Clazy found :warning: {warnings_count} warnings and "
f":pencil2: {notes_count} notes for porting to QT6</summary>"
)
elif notes_count > 0:
report_lines.append(
f"<details><summary>:pencil2: Clazy found :pencil2: {notes_count} notes for porting to QT6</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Clazy found no errors, warnings or notes for porting to QT6</summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run Clazy checks for QT6 on provided C++ files and append a Markdown report."
)
parser.add_argument(
"--files",
required=True,
help="A space-separated list (or glob-expanded string) of C++ files to check.",
)
parser.add_argument(
"--clazy-qt6-checks",
required=True,
help="Comma-separated list of clazy QT6 checks to run.",
)
parser.add_argument(
"--log-dir", required=True, help="Directory where the log file will be written."
)
parser.add_argument(
"--report-file",
required=True,
help="Path to the Markdown report file to append results.",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")
args = parser.parse_args()
# Initialize logging and required directories using a shared utility.
init_environment(args)
build_dir = "build"
fix_file = "clazyQT6.yaml"
# Build the clazy command as a list.
cmd = [
"clazy-standalone",
f"--export-fixes={fix_file}",
f"-checks={args.clazy_qt6_checks}",
"-p",
build_dir,
] + args.files.split()
stdout, stderr, _ = run_command(cmd)
output = stdout + stderr
log_file = os.path.join(args.log_dir, "clazyQT6.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "clang.json", "clang")
error_pattern = r"^.+:\d+:\d+: error: .+$"
warning_pattern = r"^.+:\d+:\d+: warning: .+$"
note_pattern = r"^.+:\d+:\d+: note: .+$"
errors_count = count_occurrences(error_pattern, output)
warnings_count = count_occurrences(warning_pattern, output)
notes_count = count_occurrences(note_pattern, output)
logging.info(
f"Found {errors_count} errors, {warnings_count} warnings, {notes_count} notes"
)
report = generate_markdown_report(output, errors_count, warnings_count, notes_count)
append_file(args.report_file, report + "\n")
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

111
tools/lint/codespell.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import urllib.request
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def download_dictionary(url, dest):
logging.info(f"Downloading dictionary from {url} to {dest}")
try:
urllib.request.urlretrieve(url, dest)
except Exception as e:
logging.error(f"Error downloading dictionary: {e}", file=sys.stderr)
sys.exit(1)
def generate_markdown_report(misspellings: int, log_file: str) -> str:
"""
Generate a Markdown report section based on the codespell results and log file.
"""
report_lines = []
if misspellings > 0:
report_lines.append(
f"<details><summary>:pencil2: Codespell found {misspellings} misspellings</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: Codespell found no misspellings</summary>"
)
report_lines.append("")
report_lines.append(
"To ignore false positives, append the word to the [.github/codespellignore]"
"(https://github.com/FreeCAD/FreeCAD/blob/master/.github/codespellignore) file (lowercase)"
)
report_lines.append("````")
with open(log_file, "r", encoding="utf-8") as lf:
report_lines.append(lf.read())
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run codespell on files and append a Markdown report."
)
parser.add_argument(
"--ignore-words",
required=True,
help="Comma-separated list of words to ignore (from codespellignore).",
)
parser.add_argument(
"--skip", required=True, help="Comma-separated list of file patterns to skip."
)
args = parser.parse_args()
init_environment(args)
dictionary_file = "dictionary.txt"
if not os.path.exists(dictionary_file):
dictionary_url = "https://raw.githubusercontent.com/codespell-project/codespell/master/codespell_lib/data/dictionary.txt"
logging.info("Dictionary not found; downloading...")
download_dictionary(dictionary_url, dictionary_file)
run_command(["pip", "install", "-q", "codespell"], check=True)
cmd = [
"codespell",
"--quiet-level",
"3",
"--summary",
"--count",
"--ignore-words",
args.ignore_words,
"--skip",
args.skip,
"-D",
dictionary_file,
] + args.files.split()
stdout, stderr, _ = run_command(cmd)
output = stdout + "\n" + stderr + "\n"
log_file = os.path.join(args.log_dir, "codespell.log")
write_file(log_file, output)
emit_problem_matchers(log_file, "codespell.json", "codespell")
try:
misspellings = int(stdout.strip())
except ValueError:
logging.info(f"Could not parse misspellings count from output:\n{stdout}")
misspellings = 0
logging.info(f"Found {misspellings} misspellings")
report = generate_markdown_report(misspellings, log_file)
append_file(args.report_file, report + "\n")
sys.exit(0 if misspellings == 0 else 1)
if __name__ == "__main__":
main()

65
tools/lint/cpplint.cfg Normal file
View File

@@ -0,0 +1,65 @@
+build/deprecated,
-build/endif_comment,
+build/explicit_make_pair,
-build/forward_decl,
-build/header_guard,
+build/include,
-build/include_subdir,
-build/include_alpha,
-build/include_order,
+build/include_what_you_use,
+build/namespaces_headers,
+build/namespaces_literals,
-build/namespaces,
+build/printf_format,
+build/storage_class,
-legal/copyright,
+readability/alt_tokens,
-readability/braces,
+readability/casting,
+readability/check,
+readability/constructors,
+readability/fn_size,
+readability/inheritance,
+readability/multiline_comment,
+readability/multiline_string,
+readability/namespace,
-readability/nolint,
+readability/nul,
+readability/strings,
-readability/todo,
+readability/utf8,
+runtime/arrays,
+runtime/casting,
+runtime/explicit,
+runtime/int,
+runtime/init,
+runtime/invalid_increment,
+runtime/member_string_references,
+runtime/memset,
+runtime/operator,
+runtime/printf,
+runtime/printf_format,
+runtime/references,
+runtime/string,
+runtime/threadsafe_fn,
+runtime/vlog,
-whitespace/blank_line,
-whitespace/braces,
-whitespace/comma,
-whitespace/comments,
-whitespace/empty_conditional_body,
-whitespace/empty_if_body,
-whitespace/empty_loop_body,
-whitespace/end_of_line,
-whitespace/ending_newline,
-whitespace/forcolon,
-whitespace/indent,
-whitespace/indent_namespace,
-whitespace/line_length,
-whitespace/newline,
-whitespace/operators,
-whitespace/parens,
-whitespace/semicolon,
-whitespace/tab,
-whitespace/todo

90
tools/lint/cpplint.py vendored Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import re
import logging
from utils import (
run_command,
init_environment,
write_file,
append_file,
emit_problem_matchers,
add_common_arguments,
)
DEFAULT_LINE_LENGTH_LIMIT = 100
def load_cpplint_filters(conf_file="cpplint.cfg"):
"""Load cpplint filters from a configuration file."""
try:
with open(conf_file, "r", encoding="utf-8") as f:
filters = f.read().strip()
return filters
except Exception as e:
logging.warning(f"Could not load {conf_file}: {e}")
return ""
def count_cpplint_errors(log_text: str) -> int:
"""
Count the number of cpplint error/warning lines.
"""
matches = re.findall(r"\[[0-9]+\]$", log_text, re.MULTILINE)
return len(matches)
def generate_markdown_report(aggregated_output: str, errors_count: int) -> str:
"""Generate a Markdown report section based on cpplint output and error count."""
report_lines = []
if errors_count > 0:
report_lines.append(
f"<details><summary>:warning: CppLint found {errors_count} errors / warnings</summary>"
)
else:
report_lines.append(
"<details><summary>:heavy_check_mark: No cpplint errors found </summary>"
)
report_lines.append("")
report_lines.append("````")
report_lines.append(aggregated_output)
report_lines.append("````")
report_lines.append("</details>")
report_lines.append("")
return "\n".join(report_lines)
def main():
parser = argparse.ArgumentParser(
description="Run cpplint on provided C++ files and append a Markdown report."
)
add_common_arguments(parser)
args = parser.parse_args()
init_environment(args)
run_command(["pip", "install", "-q", "cpplint"], check=True)
cpplint_filters = load_cpplint_filters("cpplint.cfg")
aggregated_output = ""
for file in args.files.split():
cmd = [
"cpplint",
f"--filters={cpplint_filters}",
f"--linelength={DEFAULT_LINE_LENGTH_LIMIT}",
file,
]
stdout, stderr, _ = run_command(cmd)
aggregated_output += stdout + stderr + "\n"
cpplint_log = os.path.join(args.log_dir, "cpplint.log")
write_file(cpplint_log, aggregated_output)
emit_problem_matchers(cpplint_log, "cpplint.json", "cpplint")
errors_count = count_cpplint_errors(aggregated_output)
logging.info(f"Found {errors_count} cpplint errors")
report = generate_markdown_report(aggregated_output, errors_count)
append_file(args.report_file, report)
sys.exit(0 if errors_count == 0 else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import argparse
import os
import re
import sys
from utils import (
add_common_arguments,
init_environment,
write_file,
append_file,
emit_problem_matchers,
)
def check_qt_connections(file_path):
"""
Scan the file for lines containing 'SIGNAL' or 'SLOT' and return a list of matching lines
with line numbers and a suggestion.
"""
matches = []
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
for lineno, line in enumerate(f, start=1):
if re.search(r"\b(SIGNAL|SLOT)\b", line):
matches.append(
f"{file_path}:{lineno}: {line.rstrip()} <-- Consider using functor-based connections"
)
except Exception as e:
logging.debug(f"Error reading {file_path}: {e}")
return matches
def generate_markdown_report(all_matches):
"""Generate a Markdown report based on the list of QT connection matches."""
count = len(all_matches)
if count > 0:
md_report = (
f"<details><summary>:information_source: Found {count} QT string-based connections → consider using QT functor-based connections</summary>\n"
"\nFor more information see: https://wiki.qt.io/New_Signal_Slot_Syntax or https://github.com/FreeCAD/FreeCAD/issues/6166\n"
"```\n" + "\n".join(all_matches) + "\n```\n</details>\n\n"
)
else:
md_report = ":heavy_check_mark: No string-based connections found\n\n"
return md_report
def main():
parser = argparse.ArgumentParser(
description="Check for old Qt string-based connections in files and append a Markdown report."
)
add_common_arguments(parser)
args = parser.parse_args()
init_environment(args)
all_matches = []
for file_path in args.files.split():
logging.debug(f"Checking file: {file_path}")
matches = check_qt_connections(file_path)
all_matches.extend(matches)
log_file = os.path.join(args.log_dir, "qtConnections.log")
write_file(log_file, "\n".join(all_matches))
emit_problem_matchers(log_file, "grepMatcherWarning.json", "grepMatcher-warning")
count = len(all_matches)
logging.info(f"Found {count} QT string-based connections")
md_report = generate_markdown_report(all_matches)
append_file(args.report_file, md_report)
sys.exit(0 if count == 0 else 1)
if __name__ == "__main__":
main()