Custom Java Plugin - What classes are needed? (question details enclosed) (Please help)

Hi all,

Would someone please advise?

I am developing a custom plugin to scan java test files one by one, compute a ratio at the class level with the goal of displaying that ratio next to each class inside of SonarQube.

Unable to find much online saying exactly what classes are needed for this. Have the example plugin but no examples what-so-ever similar to what I need.

Question:
What classes will I need to have in general?
e.g.(Class that implements Plugin, Registrar, Rules List, Sensor, Metric)?

In the sensor’s execute() method, how to scan file one by one and for each, have it hit “visitMethod” via the BaseTreeVisitor?

PLEASE HELP! I have other posts here that no one is responding to and I really need someone to help.

Hello @jmcnally2020,

While I understand your enthusiasm and eagerness to enter the beautiful world of rule development, let me remind you that this is an open community with people volunteering their free time to provide assistance. We’re eager to contribute to the community, but you are not guaranteed a fast response. (from the Community Guidelines).

If I understand your situation correctly, I believe this tutorial will answer many of your questions. It contains a template project, an explanation to set it up and an introduction to the API.

Please note that there is a bug preventing to use this custom plugin with the latest SonarQube version. We are working on it as we speak, but if you want to start right away, you can use the version listed in the tutorial.

Hi Quentin,

Thank you for the tutorial link.
I have gone through this previously; however, its very much inline with the other things available on the internet. There is nothing really explaining how to compute a measure at a class level (a percentage value) and return it back to the sensor to push to SonarQube so that percentage displays next to the class it just scanned.

Right now, I’m using the sonar-example-plugin 7.9.* and even that project doesn’t have any examples that do something similar.

Just to show you what I have in a nutshell. Could you tell me how far off I am and provide direction on how I can tie it together to compute a measure per scanned file, return it and save it for SonarQube to display (and also report issues in the middle of the scan).

Note: I haven’t cleaned this up yet to remove what is not needed and to add a logger vs using Sys.out. Please disregard.

@Rule(
  key = "AssertToInvokeRatioRule",
  name = "Compare Method Invocations to Unit Test Assertions",
  description = "For every method invocation in a unit test in a test class, there should be one or more unit test assertions.",
  tags = {"bug"}
 )

public class AssertToInvokeRule extends BaseTreeVisitor implements JavaFileScanner {

private static int assertionCount = 0;
private static int methodInvocationCount = 0;
private static int methodTestedClassCount = 0;
private static final String VERIFY = "verify";
private static final String ASSERT_NAME = "assert";

private static final TypeCriteria ORG_MOCKITO_MOCKITO = TypeCriteria.is("org.mockito.Mockito");
private static final TypeCriteria ORG_ASSERTJ_ASSERTIONS = TypeCriteria.is("org.assertj.core.api.Assertions");
private static final TypeCriteria ORG_ASSERTJ_FAIL = TypeCriteria.is("org.assertj.core.api.Fail");

private static final TypeCriteria ANY_TYPE = TypeCriteria.anyType();
private static final NameCriteria ANY_NAME = NameCriteria.any();
private static final NameCriteria STARTS_WITH_FAIL = NameCriteria.startsWith("fail");
private static final NameCriteria STARTS_WITH_ASSERT = NameCriteria.startsWith(ASSERT_NAME);

private final Deque<Boolean> methodContainsAssertion = new ArrayDeque<>();
private final Deque<Boolean> inUnitTest = new ArrayDeque<>();
private final Deque<Boolean> methodContainsTestedClass = new ArrayDeque<>();
private JavaFileScannerContext context;

@Override
public void scanFile(final JavaFileScannerContext context) {
	this.context = context;
	inUnitTest.push(false);
	methodContainsAssertion.push(false);
	methodContainsTestedClass.push(false);
	scan(context.getTree());
	inUnitTest.pop();
	methodContainsAssertion.pop();
	methodContainsTestedClass.pop();

	double assertToInvokeRatioResult = getAssertToInvokeRatioResult(assertionCount, methodTestedClassCount);
	DecimalFormat df = new DecimalFormat("#");
	System.out.println("********CLASS LEVEL ASSERT TO INVOKE RATIO: " + df.format(assertToInvokeRatioResult) + "%");

	 if (assertToInvokeRatioResult<80) {
		 context.reportIssue(this,context.getTree(),"Total class level method invocation count is greater than test assertion by more than 20%");
	  }
}

private double getAssertToInvokeRatioResult(int assertCount, int methodTestedCount) {
	double assertToInvokeRatioResult;
	if (assertCount >= methodTestedCount) {
		assertToInvokeRatioResult = 100;
	} else {
		assertToInvokeRatioResult = (double) 100 * assertCount / methodTestedCount;
	}
	return assertToInvokeRatioResult;
}


@Override
public void visitMethod(MethodTree methodTree) {
	if (ModifiersUtils.hasModifier(methodTree.modifiers(), Modifier.ABSTRACT)) {
		return;
	}
	boolean isUnitTest = isUnitTest(methodTree);
	inUnitTest.push(isUnitTest);
	double methodLevelRatioResult;
	int tempAssertCount = assertionCount;
	int tempMethodInvokeCount = methodTestedClassCount;

	methodContainsAssertion.push(false);
	methodContainsTestedClass.push(false);
	super.visitMethod(methodTree);
	methodContainsAssertion.pop();
	methodContainsTestedClass.pop();
	inUnitTest.pop();

	if(isUnitTest){
		tempAssertCount = assertionCount - tempAssertCount;
		tempMethodInvokeCount = methodTestedClassCount - tempMethodInvokeCount;
		methodLevelRatioResult = getAssertToInvokeRatioResult(tempAssertCount, tempMethodInvokeCount);
		DecimalFormat df = new DecimalFormat("##");
		System.out.println("********METHOD LEVEL ASSERT TO INVOKE RATIO:: " + df.format(methodLevelRatioResult) + "%");
		if(methodLevelRatioResult < 80) {
			//context.addIssueOnProject((JavaCheck) methodTree, " Method invocation count is greater than test assertion by more than 20%");
			context.reportIssue(this, methodTree.simpleName(), "Method level invocation count is greater than test assertion by more than 20%");
		}
	}
}

@Override
public void visitMethodInvocation(MethodInvocationTree mit) {
	super.visitMethodInvocation(mit);
	String fullyQualifiedNameOfExpression = new String();
	
	if (!mit.methodSelect().is(Tree.Kind.IDENTIFIER)) {
		MemberSelectExpressionTree memberSelectExpressionTree = ((MemberSelectExpressionTree) mit.methodSelect());
		fullyQualifiedNameOfExpression = memberSelectExpressionTree.expression().symbolType().fullyQualifiedName();
	}
	
	if (inUnitTest.peek() && ASSERTION_INVOCATION_MATCHERS.anyMatch(mit)) {
		assertionCount = assertionCount +1;
	}
	//if method is a UT and not an assertion, then increment methodinvocation count 
	if(inUnitTest.peek() && !ASSERTION_INVOCATION_MATCHERS.anyMatch(mit)) {
	    methodInvocationCount = methodInvocationCount +1;
	    
	    List<String> classPath = Arrays.asList(fullyQualifiedNameOfExpression.split("\\."));
	    if(classPath.get(0).equals("com") && classPath.get(1).equals("aString")){
			if (getTestedClassMethodInvocationMatchers(fullyQualifiedNameOfExpression).anyMatch(mit)){
				methodTestedClassCount = methodTestedClassCount +1;
			}
		}
	}
}

That logic, I need it to run in a sensor, and I need the computed ratio to return back to sensor at the class level and am incredibly stuck. Unable to find any examples anywhere to assist. The API is also undocumented.

@Override
  public void execute(SensorContext context) {
FileSystem fs = context.fileSystem();
Iterable<InputFile> javaFiles = fs.inputFiles(fs.predicates().hasLanguage("java"));
for (InputFile javaFile : javaFiles) {
  NewIssue newIssue = context.newIssue()
    .forRule(AssertToInvokeRulesDefinition.RULE_ON_ASSERT_TO_INVOKE_RATIO);
       etc.....
}
  }
}

Any advice you can give would be really apperciated. I’m not sure if I’m on the right track, or not. Just feeling lost and at the moment, I’m at the point where I don’t have any idea how to figure out where to go with this. I’ve scoured the internet in search of any explanation and example that could speak to what I’m trying to accomplish.

Is anyone available to respond? @Quentin @michael.edwards