cargo-quality 0.2.0

Professional Rust code quality analysis tool with hardcoded standards
Documentation
# SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
# SPDX-License-Identifier: MIT

name: 'Cargo Quality Analyzer'
description: 'Professional Rust code quality analysis with hardcoded standards'
author: 'RAprogramm'

branding:
  icon: 'check-circle'
  color: 'blue'

inputs:
  path:
    description: 'Path to analyze'
    required: false
    default: 'src/'
  analyzer:
    description: 'Specific analyzer to run (path_import, format_args, empty_lines, inline_comments, mod_rs)'
    required: false
    default: ''
  fail_on_issues:
    description: 'Fail the action if issues are found'
    required: false
    default: 'true'
  post_comment:
    description: 'Post analysis results as PR comment'
    required: false
    default: 'false'
  update_comment:
    description: 'Update existing comment instead of creating new'
    required: false
    default: 'true'

outputs:
  total_issues:
    description: 'Total number of issues found'
    value: ${{ steps.analyze.outputs.total_issues }}
  path_import_issues:
    description: 'Issues from path_import analyzer'
    value: ${{ steps.analyze.outputs.path_import_issues }}
  format_args_issues:
    description: 'Issues from format_args analyzer'
    value: ${{ steps.analyze.outputs.format_args_issues }}
  empty_lines_issues:
    description: 'Issues from empty_lines analyzer'
    value: ${{ steps.analyze.outputs.empty_lines_issues }}
  inline_comments_issues:
    description: 'Issues from inline_comments analyzer'
    value: ${{ steps.analyze.outputs.inline_comments_issues }}
  mod_rs_issues:
    description: 'Issues from mod_rs analyzer'
    value: ${{ steps.analyze.outputs.mod_rs_issues }}
  has_issues:
    description: 'Whether any issues were found'
    value: ${{ steps.analyze.outputs.has_issues }}

runs:
  using: 'composite'
  steps:
    - name: Setup Rust
      uses: dtolnay/rust-toolchain@stable

    - name: Cache cargo-quality
      uses: actions/cache@v5
      with:
        path: |
          ~/.cargo/bin/cargo-qual
          ${{ github.action_path }}/target
        key: cargo-quality-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
        restore-keys: |
          cargo-quality-${{ runner.os }}-

    - name: Build cargo-quality
      shell: bash
      run: |
        cd ${{ github.action_path }}
        if [ ! -f target/release/cargo-qual ]; then
          cargo build --release
        fi

    - name: Run analysis
      id: analyze
      shell: bash
      run: |
        ANALYZER_ARG=""
        if [ -n "${{ inputs.analyzer }}" ]; then
          ANALYZER_ARG="-a ${{ inputs.analyzer }}"
        fi

        OUTPUT=$(${{ github.action_path }}/target/release/cargo-qual check ${{ inputs.path }} $ANALYZER_ARG 2>&1) || true

        echo "Analysis output:"
        echo "$OUTPUT"

        TOTAL=0
        PATH_IMPORT=0
        FORMAT_ARGS=0
        EMPTY_LINES=0
        INLINE_COMMENTS=0
        MOD_RS=0

        if echo "$OUTPUT" | grep -q "\[path_import\]"; then
          PATH_IMPORT=$(echo "$OUTPUT" | grep -oP '\[path_import\].*?(\d+) issues' | grep -oP '\d+' | head -1 || echo "0")
        fi
        if echo "$OUTPUT" | grep -q "\[format_args\]"; then
          FORMAT_ARGS=$(echo "$OUTPUT" | grep -oP '\[format_args\].*?(\d+) issues' | grep -oP '\d+' | head -1 || echo "0")
        fi
        if echo "$OUTPUT" | grep -q "\[empty_lines\]"; then
          EMPTY_LINES=$(echo "$OUTPUT" | grep -oP '\[empty_lines\].*?(\d+) issues' | grep -oP '\d+' | head -1 || echo "0")
        fi
        if echo "$OUTPUT" | grep -q "\[inline_comments\]"; then
          INLINE_COMMENTS=$(echo "$OUTPUT" | grep -oP '\[inline_comments\].*?(\d+) issues' | grep -oP '\d+' | head -1 || echo "0")
        fi
        if echo "$OUTPUT" | grep -q "\[mod_rs\]"; then
          MOD_RS=$(echo "$OUTPUT" | grep -oP '\[mod_rs\].*?(\d+) issues' | grep -oP '\d+' | head -1 || echo "0")
        fi

        TOTAL=$((PATH_IMPORT + FORMAT_ARGS + EMPTY_LINES + INLINE_COMMENTS + MOD_RS))

        echo "total_issues=$TOTAL" >> $GITHUB_OUTPUT
        echo "path_import_issues=$PATH_IMPORT" >> $GITHUB_OUTPUT
        echo "format_args_issues=$FORMAT_ARGS" >> $GITHUB_OUTPUT
        echo "empty_lines_issues=$EMPTY_LINES" >> $GITHUB_OUTPUT
        echo "inline_comments_issues=$INLINE_COMMENTS" >> $GITHUB_OUTPUT
        echo "mod_rs_issues=$MOD_RS" >> $GITHUB_OUTPUT

        if [ "$TOTAL" -gt 0 ]; then
          echo "has_issues=true" >> $GITHUB_OUTPUT
        else
          echo "has_issues=false" >> $GITHUB_OUTPUT
        fi

        echo "OUTPUT<<EOF" >> $GITHUB_ENV
        echo "$OUTPUT" >> $GITHUB_ENV
        echo "EOF" >> $GITHUB_ENV

        if [ "${{ inputs.fail_on_issues }}" = "true" ] && [ "$TOTAL" -gt 0 ]; then
          echo "::error::Found $TOTAL code quality issues"
          exit 1
        fi

    - name: Post PR comment
      if: inputs.post_comment == 'true' && github.event_name == 'pull_request'
      shell: bash
      env:
        GH_TOKEN: ${{ github.token }}
      run: |
        MARKER="<!-- cargo-quality-report -->"
        PR_NUMBER=${{ github.event.pull_request.number }}

        TOTAL=${{ steps.analyze.outputs.total_issues }}
        PATH_IMPORT=${{ steps.analyze.outputs.path_import_issues }}
        FORMAT_ARGS=${{ steps.analyze.outputs.format_args_issues }}
        EMPTY_LINES=${{ steps.analyze.outputs.empty_lines_issues }}
        INLINE_COMMENTS=${{ steps.analyze.outputs.inline_comments_issues }}
        MOD_RS=${{ steps.analyze.outputs.mod_rs_issues }}

        # Build verdict section
        if [ "$TOTAL" -eq 0 ]; then
          VERDICT="> [!TIP]
        > **All quality checks passed.** Code meets all standards!"
        else
          VERDICT="> [!CAUTION]
        > **Found $TOTAL code quality issue(s).** Please review and fix before merging.
        >
        > - Run \`cargo qual check\` locally to see detailed locations"
        fi

        # Status icons for table
        get_status() {
          if [ "$1" -eq 0 ]; then echo "✅"; else echo "❌"; fi
        }

        STATUS_PATH=$(get_status $PATH_IMPORT)
        STATUS_FORMAT=$(get_status $FORMAT_ARGS)
        STATUS_EMPTY=$(get_status $EMPTY_LINES)
        STATUS_INLINE=$(get_status $INLINE_COMMENTS)
        STATUS_MOD_RS=$(get_status $MOD_RS)

        # Build comment with professional UI
        COMMENT="$MARKER
        ## Cargo Quality Analysis

        $VERDICT

        <details>
        <summary><strong>Results</strong> — issues found by each analyzer</summary>

        > *Each analyzer checks for specific code patterns. Zero issues means the check passed.*

        | Analyzer | Issues | Status |
        |:---------|-------:|:------:|
        | \`path_import\` | $PATH_IMPORT | $STATUS_PATH |
        | \`format_args\` | $FORMAT_ARGS | $STATUS_FORMAT |
        | \`empty_lines\` | $EMPTY_LINES | $STATUS_EMPTY |
        | \`inline_comments\` | $INLINE_COMMENTS | $STATUS_INLINE |
        | \`mod_rs\` | $MOD_RS | $STATUS_MOD_RS |
        | **Total** | **$TOTAL** | |

        </details>

        <details>
        <summary><strong>Analyzers</strong> — what each check does</summary>

        > *These analyzers enforce consistent code style and best practices.*

        | Analyzer | Description | How to fix |
        |:---------|:------------|:-----------|
        | \`path_import\` | Detects inline path imports like \`std::path::Path\` | Use \`use\` statements at the top |
        | \`format_args\` | Detects \`format!(\"{}\", x)\` patterns | Use \`format!(\"{x}\")\` syntax |
        | \`empty_lines\` | Detects empty lines inside function bodies | Remove extra blank lines |
        | \`inline_comments\` | Detects \`// comment\` inside functions | Move to doc comments or remove |
        | \`mod_rs\` | Detects \`mod.rs\` files (use modern module style) | Rename \`foo/mod.rs\` to \`foo.rs\` |

        </details>

        <details>
        <summary><strong>Raw Output</strong> — detailed analysis log</summary>

        \`\`\`
        $OUTPUT
        \`\`\`

        </details>

        ---
        <sub>[Cargo Quality](https://github.com/RAprogramm/cargo-quality)</sub>"

        if [ "${{ inputs.update_comment }}" = "true" ]; then
          EXISTING=$(gh api repos/${{ github.repository }}/issues/${PR_NUMBER}/comments \
            --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1)

          if [ -n "$EXISTING" ]; then
            gh api repos/${{ github.repository }}/issues/comments/${EXISTING} \
              -X PATCH -f body="$COMMENT"
            echo "Updated existing comment"
          else
            gh pr comment $PR_NUMBER --body "$COMMENT"
            echo "Created new comment"
          fi
        else
          gh pr comment $PR_NUMBER --body "$COMMENT"
          echo "Created new comment"
        fi