Hi Sonar team,
I encountered an analyzer crash while scanning a Java class that implements a distributed lock with a local ReentrantLock fallback.
Environment:
[INFO] --- sonar:5.5.0.6356:sonar (default-cli) @ wiki ---
Warning: Using an unspecified version instead of an explicit plugin version may introduce breaking analysis changes at an unwanted time. It is highly recommended to use an explicit version, e.g. 'org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356'.
[INFO] Java 25.0.2 Azul Systems, Inc. (64-bit)
[INFO] Linux 4.18.0-477.10.1.el8_8.x86_64 (amd64)
[INFO] Communicating with SonarQube Community Build 26.2.0.119303
[INFO] JRE provisioning: os[linux], arch[x86_64]
[INFO] Starting SonarScanner Engine...
OS: RockyLinux 8
Arch: x86_64
Scanner: Maven
CI Platform: Github/Gitlab
Code Example:
package com.***.wiki.infrastructure.cache;
import com.***.wiki.common.constant.CacheKeys;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class RedisDistributedLock {
private final StringRedisTemplate redisTemplate;
private final DefaultRedisScript<Long> releaseLockRedisScript;
private final ConcurrentMap<String, ReentrantLock> localLocks = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> localLockTokens = new ConcurrentHashMap<>();
public RedisDistributedLock(StringRedisTemplate redisTemplate,
DefaultRedisScript<Long> releaseLockRedisScript) {
this.redisTemplate = redisTemplate;
this.releaseLockRedisScript = releaseLockRedisScript;
}
public LockToken lock(String key, Duration leaseDuration, Duration waitDuration) {
String lockKey = CacheKeys.LOCK_PREFIX + key;
String token = UUID.randomUUID().toString();
long deadlineNanos = System.nanoTime() + waitDuration.toNanos();
while (System.nanoTime() < deadlineNanos) {
try {
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, token, leaseDuration);
if (Boolean.TRUE.equals(success)) {
return new LockToken(lockKey, token);
}
} catch (Exception ex) {
return localLock(lockKey, token, waitDuration);
}
try {
TimeUnit.MILLISECONDS.sleep(25);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
return null;
}
public void unlock(LockToken token) {
if (token == null) {
return;
}
if (token.key().startsWith("local:")) {
String localKey = token.key().substring("local:".length());
String expected = localLockTokens.get(localKey);
if (token.value().equals(expected)) {
localLockTokens.remove(localKey);
ReentrantLock localLock = localLocks.get(localKey);
if (localLock != null && localLock.isHeldByCurrentThread()) {
localLock.unlock();
}
}
return;
}
try {
redisTemplate.execute(releaseLockRedisScript, List.of(token.key()), token.value());
} catch (Exception ignored) {
// Best effort unlock for Redis mode.
}
}
private LockToken localLock(String lockKey, String token, Duration waitDuration) {
ReentrantLock localLock = localLocks.computeIfAbsent(lockKey, key -> new ReentrantLock());
try {
boolean acquired = localLock.tryLock(waitDuration.toMillis(), TimeUnit.MILLISECONDS);
if (!acquired) {
return null;
}
localLockTokens.put(lockKey, token);
return new LockToken("local:" + lockKey, token);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return null;
}
}
public record LockToken(String key, String value) {
}
}
Actual result:
The Java analyzer crashes with NullPointerException during symbolic execution instead of reporting an issue normally.
original error log:
Error: Unable to run check class org.sonar.java.se.SymbolicExecutionVisitor - on file 'src/main/java/com/***/wiki/infrastructure/cache/RedisDistributedLock.java', To help improve the SonarSource Java Analyzer, please report this problem to SonarSource: see https://community.sonarsource.com/
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Unknown Source)
at org.sonarsource.analyzer.commons.collections.AVLTree.get(AVLTree.java:80)
at org.sonar.java.se.ProgramState.addConstraint(ProgramState.java:318)
at org.sonar.java.se.ProgramState.addConstraintTransitively(ProgramState.java:297)
at org.sonar.java.se.checks.LocksNotUnlockedCheck$PostStatementVisitor.visitMethodInvocationWithIdentifierTarget(LocksNotUnlockedCheck.java:153)
at org.sonar.java.se.checks.LocksNotUnlockedCheck$PostStatementVisitor.visitMethodInvocation(LocksNotUnlockedCheck.java:144)
at org.sonar.java.model.expression.MethodInvocationTreeImpl.accept(MethodInvocationTreeImpl.java:101)
at org.sonar.java.se.checks.LocksNotUnlockedCheck.checkPostStatement(LocksNotUnlockedCheck.java:175)
at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:110)
at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:122)
at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:122)
at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
at org.sonar.java.se.checks.NullDereferenceCheck.checkPostStatement(NullDereferenceCheck.java:399)
at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:110)
at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
at org.sonar.java.se.CheckerDispatcher.executeCheckPostStatement(CheckerDispatcher.java:73)
at org.sonar.java.se.ExplodedGraphWalker.executeMethodInvocation(ExplodedGraphWalker.java:847)
at org.sonar.java.se.ExplodedGraphWalker.visit(ExplodedGraphWalker.java:668)
at org.sonar.java.se.ExplodedGraphWalker.execute(ExplodedGraphWalker.java:259)
at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:216)
at org.sonar.java.se.SymbolicExecutionVisitor.execute(SymbolicExecutionVisitor.java:68)
at org.sonar.java.se.xproc.BehaviorCache.get(BehaviorCache.java:92)
at org.sonar.java.se.xproc.BehaviorCache.get(BehaviorCache.java:75)
at org.sonar.java.se.ExplodedGraphWalker.executeMethodInvocation(ExplodedGraphWalker.java:811)
at org.sonar.java.se.ExplodedGraphWalker.visit(ExplodedGraphWalker.java:668)
at org.sonar.java.se.ExplodedGraphWalker.execute(ExplodedGraphWalker.java:259)
at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:216)
at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:208)
at org.sonar.java.se.SymbolicExecutionVisitor.execute(SymbolicExecutionVisitor.java:71)
at org.sonar.java.se.SymbolicExecutionVisitor.visitMethod(SymbolicExecutionVisitor.java:57)
at org.sonar.java.model.declaration.MethodTreeImpl.accept(MethodTreeImpl.java:228)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:31)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.visitClass(BaseTreeVisitor.java:67)
at org.sonar.java.model.declaration.ClassTreeImpl.accept(ClassTreeImpl.java:238)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:31)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.visitCompilationUnit(BaseTreeVisitor.java:49)
at org.sonar.java.model.JavaTree$CompilationUnitTreeImpl.accept(JavaTree.java:203)
at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
at org.sonar.java.se.SymbolicExecutionVisitor.scanFile(SymbolicExecutionVisitor.java:52)
at org.sonar.java.model.VisitorsBridge.lambda$runScanner$0(VisitorsBridge.java:286)
at org.sonar.java.model.VisitorsBridge.runScanner(VisitorsBridge.java:291)
at org.sonar.java.model.VisitorsBridge.runScanner(VisitorsBridge.java:286)
at org.sonar.java.model.VisitorsBridge.visitFile(VisitorsBridge.java:269)
at org.sonar.java.ast.JavaAstScanner.simpleScan(JavaAstScanner.java:169)
at org.sonar.java.ast.JavaAstScanner.simpleScan(JavaAstScanner.java:157)
at org.sonar.java.JavaFrontend.scanAsBatchCallback(JavaFrontend.java:247)
at org.sonar.java.JavaFrontend.lambda$scanBatch$0(JavaFrontend.java:238)
at org.sonar.java.model.JParserConfig$Batch$1.acceptAST(JParserConfig.java:187)
at org.eclipse.jdt.core.dom.CompilationUnitResolver.resolve(CompilationUnitResolver.java:1134)
at org.eclipse.jdt.core.dom.CompilationUnitResolver.resolve(CompilationUnitResolver.java:778)
at org.eclipse.jdt.core.dom.CompilationUnitResolver$ECJCompilationUnitResolver.resolve(CompilationUnitResolver.java:84)
at org.eclipse.jdt.core.dom.ASTParser.createASTs(ASTParser.java:1109)
at org.sonar.java.model.JParserConfig$Batch.parse(JParserConfig.java:173)
at org.sonar.java.JavaFrontend.scanBatch(JavaFrontend.java:238)
at org.sonar.java.JavaFrontend.scanInBatches(JavaFrontend.java:228)
at org.sonar.java.JavaFrontend.scanAsBatch(JavaFrontend.java:195)
at org.sonar.java.JavaFrontend.scan(JavaFrontend.java:160)
at org.sonar.plugins.java.JavaSensor.execute(JavaSensor.java:124)
at org.sonar.scanner.sensor.AbstractSensorWrapper.analyse(AbstractSensorWrapper.java:69)
at org.sonar.scanner.sensor.ModuleSensorsExecutor.execute(ModuleSensorsExecutor.java:88)
at org.sonar.scanner.sensor.ModuleSensorsExecutor.lambda$execute$1(ModuleSensorsExecutor.java:61)
at org.sonar.scanner.sensor.ModuleSensorsExecutor.withModuleStrategy(ModuleSensorsExecutor.java:79)
at org.sonar.scanner.sensor.ModuleSensorsExecutor.execute(ModuleSensorsExecutor.java:61)
at org.sonar.scanner.scan.SpringModuleScanContainer.doAfterStart(SpringModuleScanContainer.java:80)
at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
at org.sonar.scanner.scan.SpringProjectScanContainer.scan(SpringProjectScanContainer.java:212)
at org.sonar.scanner.scan.SpringProjectScanContainer.scanRecursively(SpringProjectScanContainer.java:208)
at org.sonar.scanner.scan.SpringProjectScanContainer.doAfterStart(SpringProjectScanContainer.java:178)
at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
at org.sonar.scanner.bootstrap.SpringScannerContainer.doAfterStart(SpringScannerContainer.java:344)
at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
at org.sonar.scanner.bootstrap.SpringGlobalContainer.doAfterStart(SpringGlobalContainer.java:143)
at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
at org.sonar.scanner.bootstrap.ScannerMain.runScannerEngine(ScannerMain.java:157)
at org.sonar.scanner.bootstrap.ScannerMain.run(ScannerMain.java:72)
at org.sonar.scanner.bootstrap.ScannerMain.main(ScannerMain.java:56)
Expected result
I would expect either:
a normal issue report from the relevant rule, or no issue but not an analyzer crash.