【Java】黒魔術を使って行数が多いメソッドを検出したらNGにするJUnit

前提: Java11, JUnit5, sbt

モンスターメソッドにうんざりしてるのでこんなものを書いてしまいました…。
プロジェクト初期にしれっと仕込んでしまいましょう。

このようなものを仕込んだ結果、
メソッド分割して１メソッド辺りの行数をとりあえず少なくしたけど、
その代わりに、色んなメソッドで参照できるように、変数のスコープを広げてグローバル変数化されかねない
という問題点もあるにはあるんですが…。
たぶん、それでも今回のこれを仕込んだほうがマシなのかなあ？とは思っています。

build.sbtのlibraryDependenciesには↓を貼り付け

```build.sbt
  "org.junit.jupiter" % "junit-jupiter-api" % "5.5.0",
  "org.junit.jupiter"%"junit-jupiter-engine" % "5.5.0",
  "org.javassist" % "javassist" % "3.25.0-GA",
```

# コード

```java
package com.github.momosetkn;

import javassist.ClassPool;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

class MonsterMethodAlert {

    @Test
    void test() throws Exception {
        var cp = ClassPool.getDefault();
        var fail = false;
        for (var className : getClassNameList()) {
            var cc = cp.get(className);
            for (var method : cc.getMethods()) {
                // java.lang.Object.equalsなど、javaパッケージ配下のメソッドは対象外
                if (method.getDeclaringClass().getName().startsWith("java"))
                    continue;
                var methodInfo = method.getMethodInfo();
                var start = methodInfo.getLineNumber(Integer.MIN_VALUE);
                var end = methodInfo.getLineNumber(Integer.MAX_VALUE);
                var line = end - start + 1;
                if (line >= 25) {
                    System.err.println(String.format("%sが%s行のモンスターメソッドとなっております", className + "#" + methodInfo.getName(), line));
                    fail = true;
                }
            }
        }
        if (fail)
            throw new Exception("モンスターメソッドが検出されました");
    }

    private List<String> getClassNameList() throws IOException, URISyntaxException {
        var list = new ArrayList<String>();
        var classLoader = Thread.currentThread().getContextClassLoader();
        var targetUrls = classLoader.getResources("");
        var CLASS_EXT = ".class";
        while (targetUrls.hasMoreElements()) {
            var url = targetUrls.nextElement();
            if (!url.getProtocol().equals("file")) {
                continue;
            }
            var targetPath = Paths.get(url.toURI());
            Files.walkFileTree(targetPath, new SimpleFileVisitor<>() {
                @Override
                public FileVisitResult visitFile(Path foundPath, BasicFileAttributes attrs) throws IOException {
                    if (foundPath.toString().endsWith(CLASS_EXT)){
                        var relativizeStr = targetPath.relativize(foundPath).toString();
                        list.add(
                                relativizeStr
                                            .substring(0, relativizeStr.length() - CLASS_EXT.length())
                                            .replace(File.separatorChar, '.')
                        );
                    }
                    return super.visitFile(foundPath, attrs);
                }
            });
        }
        return list;
    }
}
```

```実行結果例
Example#mainが36行のモンスターメソッドとなっております

java.lang.Exception: モンスターメソッドが検出されました

	at com.github.momosetkn.MonsterMethodAlert.test(MonsterMethodAlert.java:37)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:436)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:170)
	at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:166)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:113)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:112)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:55)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

```

# 解説

## メソッド行数取得

`javassist.bytecode.MethodInfo#getLineNumber`の実装を見ると、
引数に渡した数値を以下メソッドに渡しているようです。
https://github.com/jboss-javassist/javassist/blob/rel_3_25_0_ga/src/main/javassist/bytecode/LineNumberAttribute.java#L77
ループで回していって超えたかどうかでしか判定していないようなので、
`Integer.MIN_VALUE`と`Integer.MAX_VALUE`を渡しています。

startとendは、

```
100: public void method(){
101: //startはここの行数
102: //
103: //endはここの行数
104: }
```
が取れますので、103-102=2となるため、1足して3行のメソッドという扱いにしています。

## Javassistについて
古いバージョンのJavassistだと、新し目のJavaのバージョンに追従していないため、できるだけ新しくする。

# 参考資料
[パッケージ配下のクラス一覧を再帰的に探索したい \- Qiita](https://qiita.com/neko_the_shadow/items/c46b7811f56f22e6588a)
[Javassistメモ\(Hishidama's Javassist Memo\)](http://www.ne.jp/asahi/hishidama/home/tech/java/javassist.html)