LoginSignup
2
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-07

前提: Java11, JUnit5, sbt

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

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

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",

コード

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_VALUEInteger.MAX_VALUEを渡しています。

startとendは、

100: public void method(){
101: //startはここの行数
102: //
103: //endはここの行数
104: }

が取れますので、103-102=2となるため、1足して3行のメソッドという扱いにしています。

Javassistについて

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

参考資料

パッケージ配下のクラス一覧を再帰的に探索したい - Qiita
Javassistメモ(Hishidama's Javassist Memo)

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2