この記事は、CI/CD Advent Calendar 2022 の19日目の記事です。

CI/CD Advent Calendar 2022
https://qiita.com/advent-calendar/2022/cicd

ソースコードでCopy&Pasteが多いと保守性が悪くなるので、
なるべく減らすべきです。
Copy&Pasteがどの程度あるか・増加しているかの兆候は、
機械的にそのボリュームを把握できると好ましいです。

最近は、特にIaC(Infrastructure as Code)のコードに、
Copy&Pasteが多くなりがちな印象があります。

このエントリではPMDにある、CPD機能をGitHubActionから動かして、
Copy&Pasteの量を把握する仕組みの作り方を説明します。

PMD Source Code Analyzer
https://pmd.github.io/

Finding duplicated code with CPD
https://pmd.github.io/latest/pmd_userdocs_cpd.html

# 近頃、ソースコードのコピペというと、
# ブログなどからコピーしてくる事を指すことが多いようですが。
# このエントリで言うCopy&Pasteは、リポジトリ内でのコピーのことです。

出力イメージと実装

GitHubActionsのWorkflow設定、出力例は次の通りです。

出力イメージ

CopyRate: 0.9659863945578231
CopyTokenSize: 142
TotalTokenSize: 147
/home/runner/work/sandbox/sandbox/./fuga.py: 0.9795918367346939 (copy_tokens=48, total_tokens=49)
/home/runner/work/sandbox/sandbox/./hoge.py: 0.9591836734693877 (copy_tokens=94, total_tokens=98)

.github/workflows/ci-cpd.yaml

name: ci-cpd
on:
  push:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: ./.github/actions/cpd
      with:
        language: python

.github/actions/cpd/action.yaml

name: cpd
inputs:
  pmd_version:
    default: '6.52.0'
  minimum_tokens:
    default: '100'
  fail_on_violation:
    default: true
  language:
    required: true
runs:
  using: "composite"
  steps:
  - uses: actions/setup-java@v3
    with:
      distribution: 'temurin'
      java-version: '11'
  - uses: actions/setup-python@v4
    with:
      python-version: '3.10'
  - name: setup-pmd
    run: |
      export PMD_VERSION=${{ inputs.pmd_version }}
      cd $HOME
      wget -q https://github.com/pmd/pmd/releases/download/pmd_releases%2F$PMD_VERSION/pmd-bin-$PMD_VERSION.zip
      unzip -q pmd-bin-$PMD_VERSION.zip
      echo "$HOME/pmd-bin-$PMD_VERSION/bin" >> $GITHUB_PATH      
    shell: bash
  - name: run-pmd-cpd
    run: |
      run.sh cpd --dir . --minimum-tokens ${{ inputs.minimum_tokens }} --language ${{ inputs.language }} --format xml --fail-on-violation ${{ inputs.fail_on_violation }} | python -c '
      import xml.etree.ElementTree as ET
      import sys
      root = ET.fromstring(sys.stdin.read())
      report_each_file = [
          {
              "path": f.attrib["path"],
              "total": int(f.attrib["totalNumberOfTokens"]),
              "copy": len(set(sum([
                  list(range(int(d.attrib["begintoken"]),int(d.attrib["endtoken"])))
                  for d in root.findall("duplication/file")
                  if d.attrib["path"] == f.attrib["path"]
              ], [])))
          }
          for f in root.findall("file")
          if int(f.attrib["totalNumberOfTokens"]) > 0
      ]
      total_token = sum([f["total"] for f in report_each_file])
      dup_token = sum([f["copy"] for f in report_each_file])

      print("CopyRate: {}".format(float(dup_token)/total_token))
      print("CopyTokenSize: {}".format(dup_token))
      print("TotalTokenSize: {}".format(total_token))
      for f in report_each_file:
          if f["copy"] > 0:
              print("{}: {} (copy_tokens={}, total_tokens={})".format(
                  f["path"], float(f["copy"])/f["total"], f["copy"], f["total"]
              ))
      '      
    shell: bash

Workflowは、中にPythonのコードを含めて見にくくなってしまったので、
複合アクション(composite action)にしておきました。

複合アクションを作成する | GitHub Docs
https://docs.github.com/ja/actions/creating-actions/creating-a-composite-action

運用について

個人的な意見です。

既定では、CPDでCopy&Pasteを検知するとWorkflowが失敗するようになっていますが、
GitHubのPullRequestの運用では、Copy&Pasteが無いことをmergeの必須条件にしなくても良いと思います。

GitHubでの一般的なPullRequestの運用では、
CIが失敗すると、その対応はPullRequstを作成したプログラマーが行いますが。
Copy&Pasteに関しては、担当者レベルで工夫するよりも、
システム全体を把握しているアーキテクトが責任を持って、
フレームワークレベルでCopy&Pasteが不要になるように設計を見直すべきだと思います。
担当者レベルで工夫すると「船頭多くして船山に上る」状態になって、
Copy&Pasteを避けるための工夫による別の複雑さが生まれてしまうリスクがあります。

一般的なCIは、アーキテクトが各プログラマーにルールを守らせるためにチェックしますが、
この仕組みは、アーキテクトに設計見直しの責務が生まれたことを検知するために運用するのが良いのかなと。

以上。