Salesforce のカスタマイズで用いる独自言語、Apex と Visualforce のコードを静的解析できるツール PMD Apex / PMD Visualforce を使ってみたので、ハマりどころや CI の工夫などをまとめてみます。
PMD といえば、Java 界隈では言わずと知れた静的解析ツール。同じく Java 界隈でよく使われる FindBugs はソースコード (*.java
) ではなくコンパイルされたクラスファイル (*.class
) を解析対象とするため他の言語へ拡張が困難ですが、対して PMD はソースコードを AST (抽象構文木) 化して解析するため様々な言語に適用できるツールキットになるという特徴があります。
PMD が Apex/Visualforce に対応しているのを同僚から教えてもらったときは「え、対応してたんだ!」とびっくりしたものですが、思えば Apex はコンパイル&リンクを Force.com サーバーで行うため、クライアントサイドで解析するには PMD は大変良いアプローチなわけですね。
蛇足ですが、PMD ロゴに添えられている「Don’t shoot the messenger」とは英語圏のことわざで「悪い知らせを伝えてくる人に怒ってはいけない」といった意味のようです。何が言いたいかは・・・わかりますよね😄
don’t shoot the messenger Meaning in the Cambridge English Dictionary
https://dictionary.cambridge.org/dictionary/english/don-t-shoot-the-messenger
さて、本記事は事前条件として Apex/Visualforce ソースコードが手元にある場面を想定しています。以下のようなツールが役立つでしょう。Salesforce DX はとってもエンジニアフレンドリーですし積極採用していきたいところですね✨
それでは PMD をセットアップしてみたいと思います。素晴らしいことに PMD のディストリビューションには最初から Apex も Visualforce も備わっています。従って、あなたがすべきことは PMD をダウンロードし、ソースコードの在り処、期待する出力フォーマット、そして適用したいルールセットを添えて実行するだけです。
cd $HOME
wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F5.8.1/pmd-bin-5.8.1.zip
unzip pmd-bin-5.8.1.zip
alias pmd="$HOME/pmd-bin-5.8.1/bin/run.sh pmd"
pmd -d /path/to/src -f html -R apex-apexunit,apex-braces,apex-complexity,apex-performance,apex-security,apex-style,vf-security
備考:
-d
はいろんなメタデータのディレクトリがある上のディレクトリ (つまりsrc
) を指定すれば OK です。trigger だの component だのは指定せずとも拾ってくれます。明示する場合は-d
オプションの代わりに-fileset
オプションでパターンをカンマ区切りで指定します[参考文献1]-l
で言語を指定することができますが[参考文献1]、この例ではあってもなくても結果が変わりませんでした-f
は出力フォーマットを指定する項目で、以下の選択肢があります[参考文献1]:codeclimate
,csv
,emacs
,html
,ideaj
,summaryhtml
,text
,textcolor
,textpad
,vbhtml
,xml
,xslt
(xsltFilename
プロパティも指定),yahtml
(outputDir
プロパティも指定)
-f html
で HTML レポートを出力すると、こんな感じの質素な HTML が標準出力されます。
-f vbhtml
で HTML レポートを出力すると、こんな感じの HTML が標準出力されます。ファイルごとにまとまった感じで出力されていいですね。ちなみに vb とは Vladimir Bossicard (開発者の名前) のようです。
-f yahtml
は Yet Another HTML というモードで、ディレクトリに複数の HTML 群を吐いてくれるモードですが、残念ながら Apex/Visualforce ではいい感じの HTML が生成されませんでした。なお、出力ディレクトリは -P outputDir=xxx
で場所を教える必要があります。さらに、指定したディレクトリは既に存在する必要があります (通常は直前に mkdir
する)。
-f xml
は Jenkins のプラグインで集計する際などに使うフォーマットです。
Visual Studio Code をお使いの方は、開いたり保存したりしたときに PMD Apex を自動的に掛ける素晴らしいプラグインがあります:
ChuckJonas/vscode-apex-pmd: PMD static analysis for Apex in vscode
さて、ここまで書いたところで今更なのですが、私の手元の環境では重大な問題が発生していました。なんと、Java SE 9 では Apex 解析部分が正常に動作しないようです (PMD Java とかは問題なし)。以前出たバグの再発っぽいですが、詳細は解析中。今はひとまず Java SE 8 / OpenJDK 8 で回避しましょう。上述の VSCode プラグインも Java 9 環境では全く吠えてくれません😅
Exception in thread "main" java.lang.IncompatibleClassChangeError: Inconsistent constant pool data in classfile for class apex/jorje/semantic/symbol/member/method/MethodTable. Method lambda$static$64(Lapex/jorje/semantic/symbol/member/method/MethodInfo;)Z at index 85 is CONSTANT_MethodRef and should be CONSTANT_InterfaceMethodRef
at apex.jorje.semantic.symbol.member.method.MethodTable.<clinit>(MethodTable.java:38)
at apex.jorje.semantic.symbol.type.ModifierTypeInfo$Builder.build(ModifierTypeInfo.java:119)
at apex.jorje.semantic.symbol.type.ModifierTypeInfos.<clinit>(ModifierTypeInfos.java:54)
at apex.jorje.semantic.ast.modifier.ModifierGroups.<clinit>(ModifierGroups.java:27)
at apex.jorje.semantic.symbol.type.TypeInfos.<clinit>(TypeInfos.java:40)
at apex.jorje.semantic.symbol.member.variable.TriggerVariableMap.<clinit>(TriggerVariableMap.java:46)
at apex.jorje.semantic.symbol.resolver.StandardSymbolResolver.<init>(StandardSymbolResolver.java:80)
at apex.jorje.semantic.compiler.CompilerContext.<init>(CompilerContext.java:49)
at apex.jorje.semantic.compiler.ApexCompiler.<init>(ApexCompiler.java:57)
at apex.jorje.semantic.compiler.ApexCompiler.<init>(ApexCompiler.java:37)
at apex.jorje.semantic.compiler.ApexCompiler$Builder.build(ApexCompiler.java:210)
at net.sourceforge.pmd.lang.apex.ast.CompilerService.compile(CompilerService.java:95)
at net.sourceforge.pmd.lang.apex.ast.CompilerService.visitAstsFromStrings(CompilerService.java:90)
at net.sourceforge.pmd.lang.apex.ast.CompilerService.visitAstFromString(CompilerService.java:78)
at net.sourceforge.pmd.lang.apex.ast.ApexParser.parseApex(ApexParser.java:42)
at net.sourceforge.pmd.lang.apex.ast.ApexParser.parse(ApexParser.java:51)
at net.sourceforge.pmd.lang.apex.ApexParser.parse(ApexParser.java:37)
at net.sourceforge.pmd.SourceCodeProcessor.parse(SourceCodeProcessor.java:113)
at net.sourceforge.pmd.SourceCodeProcessor.processSource(SourceCodeProcessor.java:175)
at net.sourceforge.pmd.SourceCodeProcessor.processSourceCode(SourceCodeProcessor.java:97)
at net.sourceforge.pmd.SourceCodeProcessor.processSourceCode(SourceCodeProcessor.java:52)
at net.sourceforge.pmd.processor.PmdRunnable.call(PmdRunnable.java:88)
at net.sourceforge.pmd.processor.PmdRunnable.call(PmdRunnable.java:27)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.base/java.lang.Thread.run(Thread.java:844)
最後に、GitLab CI で回す方法を試行錯誤してみました。
基本的にやることはコマンドを叩くだけですが、先述の通り JRE のバージョンを気をつけることと、終了ステータスが 0 でなくても打ち切りにさせない方法、そして HTML レポートを artifact (成果物) として保存することの 3 点がミソです。
image: openjdk:8-jdk
stages:
- pmd
pmd:
stage: pmd
variables:
PMD_VAR: '5.8.1'
PMD_OUT: 'pmd.html'
RULESETS: 'apex-apexunit,apex-braces,apex-complexity,apex-performance,apex-security,apex-style,vf-security'
script:
- wget -q https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VAR}/pmd-bin-${PMD_VAR}.zip
- unzip -q pmd-bin-${PMD_VAR}.zip
- echo `./pmd-bin-${PMD_VAR}/bin/run.sh pmd -d src -f html -R ${RULESETS}` > $PMD_OUT
artifacts:
paths:
- $PMD_OUT
GitLab CI (他の Scriptable な CI サービスも同様) は終了ステータスが 0 か否かでステージの成功/失敗を判定し、しかも失敗の場合は後続の処理 (artifact の保存も含む) が実行されないため、何も対策しないと PMD が問題を見つけたとき (return 1 したとき) に肝心のレポートが見られなくなってしまいます。そこで、echo
とバッククオートを使って回避してみました。echo
した結果 (標準出力のみ) はファイルにリダイレクトしてレポートを作る仕組みです。
Salesforce DX などでビルドやテスト、デプロイのステージを設ける際も、pmd ステージはレポートを保存しなければいけませんから、問題を検出した際に CI として失敗としたい場合は一旦 pmd ステージは通過させた上で、後続のパイプラインで再度 (今度は小細工なしで) pmd
を実行して失敗に落とすのが良さそうです。
PMD は現在 6.0 のメジャーリリースに向けて開発が進んでいます。ルールセットのカテゴリーが整理されることになり、既存のルールセット指定が変わる可能性があります (しばらくは後方互換性のために残る模様)。5.x 系も盛んにアップデートされているため、今後も開発の行方を追っていきたいところです。