All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.java.checks.VirtualThreadNotSynchronizedCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube Java
 * Copyright (C) 2012-2025 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.java.checks;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.JavaVersion;
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.SynchronizedStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S6906")
public class VirtualThreadNotSynchronizedCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {

  @Override
  public boolean isCompatibleWithJavaVersion(JavaVersion version) {
    return version.isJava21Compatible();
  }

  @Override
  public List nodesToVisit() {
    return List.of(Tree.Kind.COMPILATION_UNIT);
  }

  @Override
  public void visitNode(Tree tree) {
    var runnablesCollector = new RunnablesToCheckCollector();
    tree.accept(runnablesCollector);

    var synchonizedTreesCollector = new TreesWithSynchronizedCodeCollector();
    runnablesCollector.invocations.stream()
      .filter(it -> synchonizedTreesCollector.isInvokingSynchronizedCode(it.arguments().get(0)))
      .forEach(it -> reportIssue(it, synchonizedTreesCollector.getSecondaryLocation(it.arguments().get(0))));
  }

  void reportIssue(MethodInvocationTree tree, Tree secondaryLocation) {
    reportIssue(
      tree.methodSelect(),
      "Use a platform thread instead of a virtual thread",
      List.of(new JavaFileScannerContext.Location("synchronized", secondaryLocation)),
      null
    );
  }

  private static class RunnablesToCheckCollector extends BaseTreeVisitor {

    private static final String OF_VIRTUAL = "java.lang.Thread$Builder$OfVirtual";

    private static final MethodMatchers VIRTUAL_THREAD_BUILDER_METHODS = MethodMatchers.or(
      MethodMatchers.create()
        .ofSubTypes("java.lang.Thread$Builder")
        .names("start", "unstarted")
        .addParametersMatcher("java.lang.Runnable").build(),
      MethodMatchers.create()
        .ofTypes("java.lang.Thread")
        .names("startVirtualThread")
        .addParametersMatcher("java.lang.Runnable").build(),
      MethodMatchers.create()
        .ofSubTypes("java.util.concurrent.ExecutorService")
        .names("execute", "submit")
        .withAnyParameters().build()
    );

    private static final MethodMatchers EXECUTOR_BUILDER_SERVICE_WITH_VIRTUAL_TASKS_METHOD = MethodMatchers.create()
      .ofTypes("java.util.concurrent.Executors")
      .names("newVirtualThreadPerTaskExecutor")
      .withAnyParameters().build();

    public final List invocations = new ArrayList<>();

    @Override
    public void visitMethodInvocation(MethodInvocationTree tree) {
      super.visitMethodInvocation(tree);
      if (VIRTUAL_THREAD_BUILDER_METHODS.matches(tree) && isRunnableInVirtualThread(tree)) {
        invocations.add(tree);
      }
    }

    private static boolean isRunnableInVirtualThread(MethodInvocationTree tree) {
      return tree.methodSelect() instanceof MemberSelectExpressionTree methodSelect &&
        switch (tree.methodSymbol().name()) {
          case "start", "unstarted" -> isCallToOfVirtual(methodSelect.expression());
          case "execute", "submit" -> isCallToExecutorServiceWithVirtualTasks(methodSelect.expression());
          default -> true;
        };
    }

    private static boolean isCallToOfVirtual(ExpressionTree callSiteExpression) {
      if (callSiteExpression.symbolType().is(OF_VIRTUAL)) {
        return true;
      }

      // Cover the case that call site is not type OfVirtual, but initialized with OfVirtual
      return getInitializerExpression(callSiteExpression)
        .stream()
        .anyMatch(it -> it.symbolType().is(OF_VIRTUAL));
    }

    private static boolean isCallToExecutorServiceWithVirtualTasks(ExpressionTree callSiteExpression) {
      if (isCallToExecutorServiceBuilderWithVirtualTasks(callSiteExpression)) {
        return true;
      }

      return getInitializerExpression(callSiteExpression)
        .stream()
        .anyMatch(RunnablesToCheckCollector::isCallToExecutorServiceBuilderWithVirtualTasks);
    }

    private static Optional getInitializerExpression(ExpressionTree expression) {
      return expression instanceof IdentifierTree identifier && identifier.symbol().declaration() instanceof VariableTree variableTree ?
        Optional.ofNullable(variableTree.initializer()) : Optional.empty();
    }

    private static boolean isCallToExecutorServiceBuilderWithVirtualTasks(ExpressionTree expression) {
      return expression instanceof MethodInvocationTree mit && EXECUTOR_BUILDER_SERVICE_WITH_VIRTUAL_TASKS_METHOD.matches(mit);
    }
  }

  private static class TreesWithSynchronizedCodeCollector extends BaseTreeVisitor {

    private Tree currentCheckedTree;

    private final Map treesWithSynchronizedCode = new HashMap<>();

    private final Set checkedTrees = new HashSet<>();

    private boolean isSynchronizedAttributeFound = false;

    public boolean isInvokingSynchronizedCode(Tree tree) {
      this.currentCheckedTree = tree;
      isSynchronizedAttributeFound = false;
      tree.accept(this);
      return treesWithSynchronizedCode.containsKey(tree);
    }

    public Tree getSecondaryLocation(Tree tree) {
      return treesWithSynchronizedCode.get(tree);
    }

    private void markSynchronizedBy(Tree secondaryLocation) {
      treesWithSynchronizedCode.put(currentCheckedTree, secondaryLocation);
      isSynchronizedAttributeFound = true;
    }

    @Override
    public void visitSynchronizedStatement(SynchronizedStatementTree tree) {
      if (!isSynchronizedAttributeFound) {
        markSynchronizedBy(tree.synchronizedKeyword());
      }
    }

    @Override
    public void visitMethodInvocation(MethodInvocationTree tree) {
      if (isSynchronizedAttributeFound) {
        return;
      }
      if (tree.methodSymbol().isSynchronizedMethod()) {
        markSynchronizedBy(tree);
        return;
      }
      // Make sure to visit arguments even for methods without declaration
      super.visitMethodInvocation(tree);

      var declaration = tree.methodSymbol().declaration();
      if (declaration != null) {
        checkNonSynchronizedMethod(declaration);
      }
    }

    void checkNonSynchronizedMethod(MethodTree method) {
      var methodMarkedSynchronizedBy = treesWithSynchronizedCode.get(method);
      if (methodMarkedSynchronizedBy != null) {
        markSynchronizedBy(methodMarkedSynchronizedBy);
        return;
      }

      if (checkedTrees.contains(method)) {
        return;
      }
      checkedTrees.add(method);

      var block = method.block();
      if (block == null) {
        return;
      }
      var restoreCurrentCheckedTree = currentCheckedTree;
      currentCheckedTree = method;
      block.accept(this);
      currentCheckedTree = restoreCurrentCheckedTree;
      if (isSynchronizedAttributeFound) {
        markSynchronizedBy(treesWithSynchronizedCode.get(method));
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy