GitHubActionsでCPDを動かしてCopy&Pasteを集計する
この記事は、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は、アーキテクトが各プログラマーにルールを守らせるためにチェックしますが、
この仕組みは、アーキテクトに設計見直しの責務が生まれたことを検知するために運用するのが良いのかなと。
以上。