Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 35 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ Additionally, some Error Prone plugins are included:

## Usage

Enable a quality profile including some rules, for NullAway you will need to configure the list of annotated packages
Enable a quality profile including some rules from the Error Prone, NullAway, SLF4J, or Picnic repositories.

## Compatibility

The plugin is compatible with SonarQube from version 9.9 LTS through 10.x and 25.x
The plugin is compatible with SonarQube 9.9 LTS, 10.x, 25.x, and 26.x.

The Sonar analyzer and Error Prone must run on JDK 11 or newer but can analyze Java 8 code.
When running on JDK 16 or newer add the following options due to [JEP 396](https://openjdk.java.net/jeps/396):
The analysis must run on JDK 17 or newer. Due to [JEP 396](https://openjdk.java.net/jeps/396), the following JVM options are required for the scanner process:

```
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
Expand All @@ -32,24 +31,43 @@ When running on JDK 16 or newer add the following options due to [JEP 396](https
--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
```

See [.mvn/jvm.config](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/.mvn/jvm.config) for a way to do it with Maven and [gradle.properties](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/gradle.properties) for a way to do it with Gradle
See [.mvn/jvm.config](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/.mvn/jvm.config) for Maven and [gradle.properties](sonar-erroraway-sonar-plugin/src/test/resources/projects/simple/gradle.properties) for Gradle examples.

From SonarQybe 10.6 the scanner also auto provisions a JRE and runs the analysis off that JVM. Since the JRE does not include the required compiler module, this needs to be disabled with `sonar.scanner.skipJreProvisioning=true`.
From SonarQube 10.6, the scanner auto-provisions a JRE without the `jdk.compiler` module. Disable this with `sonar.scanner.skipJreProvisioning=true`.

When these options are not set you will receive errors:
Without these options you will see:
```
Exception in thread "main" java.util.ServiceConfigurationError: com.google.errorprone.bugpatterns.BugChecker: Provider ... could not be instantiated
...
Caused by: java.lang.IllegalAccessError: class ... (in unnamed module @...) cannot access class com.sun.tools.javac.code.Symbol (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.code to unnamed module @...
java.lang.IllegalAccessError: ... cannot access class com.sun.tools.javac.code.Symbol (in module jdk.compiler)
```

## NullAway configuration

NullAway needs to be configured with the `nullaway.annotated.packages` option, for instance:

```
nullaway.annotated.packages=com.foo,org.bar
```
## Configuration

Options can be set in two places:
1. **SonarQube Server** — under Project Settings, use the keys listed below.
2. **Build tool** (Gradle/Maven) — prefix the key with `sonar.` (e.g. `sonar.nullaway.annotated.packages`). The scanner only forwards `sonar.*` properties from build tools.

### NullAway options

| Key | Type | Description |
|-----|------|-------------|
| `nullaway.annotated.packages` | multi-value | Packages to check for null safety. Required unless `nullaway.only.null.marked` is set. |
| `nullaway.unannotated.packages` | multi-value | Sub-packages to exclude from the annotated package list. |
| `nullaway.unannotated.classes` | multi-value | Classes within annotated packages to treat as unannotated. |
| `nullaway.known.initializers` | multi-value | Fully qualified method names that NullAway should treat as initializers. |
| `nullaway.field.annotations` | multi-value | Annotations that exclude fields from initialization checks. |
| `nullaway.only.null.marked` | boolean | Only check code within `@NullMarked` scope. Package-scope options are ignored when enabled. |

### ErrorAway options

| Key | Type | Description |
|-----|------|-------------|
| `erroraway.classpath.maven.coordinates` | multi-value | Maven coordinates of dependencies needed to compile the project. |
| `erroraway.annotation.processors.maven.coordinates` | multi-value | Maven coordinates of annotation processors. |
| `erroraway.maven.repositories` | multi-value | Maven remote repository URLs. |
| `erroraway.maven.local.repository` | string | Path to the Maven local repository. |
| `erroraway.maven.user.settings.file` | string | Path to the Maven user settings file. |
| `erroraway.maven.work.offline` | boolean | Let Maven work offline. |
| `erroraway.maven.use.temp.local.repository` | boolean | Use a temporary folder for the Maven local repository. |

## Developing the ErrorAway plugin

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<sonar-orchestrator.version>5.6.0.2578</sonar-orchestrator.version>

<errorprone.version>2.42.0</errorprone.version>
<nullaway.version>0.12.7</nullaway.version>
<nullaway.version>0.12.15</nullaway.version>
<errorprone.slf4j.version>0.1.29</errorprone.slf4j.version>
<picnic.errorprone.support.version>0.25.0</picnic.errorprone.support.version>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public final class ErrorAwayRulesMapping {
public static final String PICNIC_REPOSITORY = "picnic-errorprone";

public static final int ERRORPRONE_REPOSITORY_RULES_COUNT = 475;
public static final int NULLAWAY_REPOSITORY_RULES_COUNT = 1;
public static final int NULLAWAY_REPOSITORY_RULES_COUNT = 2;
public static final int ERRORPRONE_SLF4J_REPOSITORY_RULES_COUNT = 8;
public static final int PICNIC_REPOSITORY_RULES_COUNT = 45;

Expand Down
2 changes: 1 addition & 1 deletion sonar-erroraway-sonar-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.21.0.505</version>
<version>1.23.0.740</version>
<extensions>true</extensions>
<configuration>
<pluginKey>errorawaysonar</pluginKey>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package com.github.erroraway.sonarqube;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.tools.Diagnostic;
Expand All @@ -36,6 +40,8 @@

import com.github.erroraway.ErrorAwayException;
import com.github.erroraway.rules.ErrorAwayRulesMapping;
import com.google.errorprone.BugCheckerInfo;
import com.google.errorprone.bugpatterns.BugChecker;

/**
* @author Guillaume
Expand All @@ -44,6 +50,8 @@
public class ErrorAwayDiagnosticListener implements DiagnosticListener<JavaFileObject> {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorAwayDiagnosticListener.class);

private static final Map<String, String> PLUGIN_RULE_REPOSITORY_MAP = buildPluginRuleRepositoryMap();

public static final String ERROR_PRONE_COMPILER_CRASH_CODE = "compiler.err.error.prone.crash";
private static final Set<String> ERROR_PRONE_DIAGNOSTIC_CODES = Set.of("compiler.warn.error.prone",
"compiler.err.error.prone",
Expand All @@ -62,7 +70,7 @@ public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
String message = diagnostic.getMessage(Locale.ENGLISH);
String rule = parseRule(diagnostic, message);

RuleKey ruleKey = RuleKey.of(findRepository(rule, message), rule);
RuleKey ruleKey = RuleKey.of(findRepository(rule), rule);

int startLine = (int) diagnostic.getLineNumber();

Expand Down Expand Up @@ -156,18 +164,20 @@ private String parseRule(Diagnostic<? extends JavaFileObject> diagnostic, String
}
}

private String findRepository(String rule, String message) {
if (rule.startsWith("Slf4j")) {
return ErrorAwayRulesMapping.ERRORPRONE_SLF4J_REPOSITORY;
}

if (message.contains("see https://error-prone.picnic.tech/bugpatterns/")) {
return ErrorAwayRulesMapping.PICNIC_REPOSITORY;
}
private String findRepository(String rule) {
String repository = PLUGIN_RULE_REPOSITORY_MAP.get(rule);
return repository != null ? repository : ErrorAwayRulesMapping.ERRORPRONE_REPOSITORY;
}

return switch (rule) {
case "NullAway" -> ErrorAwayRulesMapping.NULLAWAY_REPOSITORY;
default ->ErrorAwayRulesMapping.ERRORPRONE_REPOSITORY;
};
private static Map<String, String> buildPluginRuleRepositoryMap() {
Map<String, String> map = new HashMap<>();
Iterator<BugChecker> checkers = ErrorAwayRulesMapping.pluginCheckers();
while (checkers.hasNext()) {
BugChecker checker = checkers.next();
String repo = ErrorAwayRulesMapping.repository(checker.getClass());
String ruleKey = BugCheckerInfo.create(checker.getClass()).canonicalName();
map.put(ruleKey, repo);
}
return Collections.unmodifiableMap(map);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,19 @@ public void define(Context context) {
.build());

for (NullAwayOption option : NullAwayOption.values()) {
context.addExtension(PropertyDefinition
PropertyDefinition.Builder builder = PropertyDefinition
.builder(option.getKey())
.name(option.getName())
.description(option.getDescription())
.category(PROPERTY_NULLAWAY_CATEGORY)
.onQualifiers(Qualifiers.PROJECT)
.multiValues(true)
.build());
.onQualifiers(Qualifiers.PROJECT);

switch (option.getType()) {
case BOOLEAN -> builder.type(PropertyType.BOOLEAN);
case STRING_ARRAY -> builder.multiValues(true);
}

context.addExtension(builder.build());
}

context.addExtension(ErrorAwayRulesDefinition.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import com.github.erroraway.ErrorAwayException;
import com.github.erroraway.rules.ErrorAwayRulesMapping;
import com.google.errorprone.BugCheckerInfo;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneJavaCompiler;
import com.google.errorprone.ErrorProneOptions;
import com.google.errorprone.bugpatterns.BugChecker;
Expand Down Expand Up @@ -92,7 +93,7 @@ public void execute(SensorContext context) {
}

// Compiler options
ErrorProneOptions errorProneOptions = buildErrorProneOptions(context);
ErrorProneOptions errorProneOptions = buildErrorProneOptions(context, checkers);
List<String> javacOptions = buildJavacOptions();

// Setup the compiler and analyze the code
Expand Down Expand Up @@ -121,14 +122,16 @@ public void execute(SensorContext context) {
}
}

private ErrorProneOptions buildErrorProneOptions(SensorContext context) {
private ErrorProneOptions buildErrorProneOptions(SensorContext context, List<Class<? extends BugChecker>> checkers) {
Configuration configuration = context.config();
List<String> options = new ArrayList<>();

boolean onlyNullMarked = getBoolean(configuration, NullAwayOption.ONLY_NULL_MARKED);

// Fail here if the option was not set instead a showing a giant stacktrace
if (!configuration.hasKey(NullAwayOption.ANNOTATED_PACKAGES.getKey())) {
if (!onlyNullMarked && !hasOption(configuration, NullAwayOption.ANNOTATED_PACKAGES)) {
if (context.activeRules().find(RuleKey.of("nullaway", "NullAway")) != null) {
throw new ErrorAwayException("The " + NullAwayOption.ANNOTATED_PACKAGES.getKey() + " option must be set when the NullAway rule is enabled");
throw new ErrorAwayException("The " + NullAwayOption.ANNOTATED_PACKAGES.getKey() + " option must be set when the NullAway rule is enabled (unless " + NullAwayOption.ONLY_NULL_MARKED.getKey() + " is set)");
}

// When some annotation processors are enabled com.google.errorprone.ErrorPronePlugins turns on plugin
Expand All @@ -139,18 +142,68 @@ private ErrorProneOptions buildErrorProneOptions(SensorContext context) {
}

for (NullAwayOption option : NullAwayOption.values()) {
String key = option.getKey();
// In OnlyNullMarked mode, skip package-scope options since NullAway would check
// both scopes if both are set
if (onlyNullMarked && option.isPackageScopeOption()) {
continue;
}

if (configuration.hasKey(key)) {
String[] values = configuration.getStringArray(key);
if (hasOption(configuration, option)) {
switch (option.getType()) {
case BOOLEAN -> {
if (getBoolean(configuration, option)) {
options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=true");
}
}
case STRING_ARRAY -> {
String[] values = getStringArray(configuration, option);
options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=" + String.join(",", values));
}
}
}
}

options.add("-XepOpt:NullAway:" + option.getErrorproneOption() + "=" + String.join(",", values));
// Override SUGGESTION-level checkers to WARN so they produce diagnostics.
// At SUGGESTION severity, ErrorProne attempts to generate auto-fix patches which requires
// com.sun.source.tree.Tree — inaccessible from the SonarQube plugin classloader.
// This crashes the entire compilation, suppressing all diagnostics from all checkers.
for (Class<? extends BugChecker> checker : checkers) {
BugCheckerInfo info = BugCheckerInfo.create(checker);
if (info.defaultSeverity() == BugPattern.SeverityLevel.SUGGESTION) {
options.add("-Xep:" + info.canonicalName() + ":WARN");
}
}

return ErrorProneOptions.processArgs(options);
}

/**
* Checks if an option is set, looking at both the server-side key and the
* analysis parameter key (sonar. prefixed).
*/
private boolean hasOption(Configuration configuration, NullAwayOption option) {
return configuration.hasKey(option.getKey()) || configuration.hasKey(option.getAnalysisParameterKey());
}

/**
* Reads a boolean option, checking both the server-side key and the analysis parameter key.
*/
private boolean getBoolean(Configuration configuration, NullAwayOption option) {
return configuration.getBoolean(option.getKey()).orElse(false)
|| configuration.getBoolean(option.getAnalysisParameterKey()).orElse(false);
}

/**
* Reads a string array option, preferring the analysis parameter key over the server-side key.
*/
private String[] getStringArray(Configuration configuration, NullAwayOption option) {
String[] values = configuration.getStringArray(option.getAnalysisParameterKey());
if (values != null && values.length > 0) {
return values;
}
return configuration.getStringArray(option.getKey());
}

private List<String> buildJavacOptions() {
List<String> options = new ArrayList<>();

Expand Down Expand Up @@ -220,9 +273,8 @@ private void addErrorProneCheckers(SensorContext context, List<Class<? extends B

@Override
public void describe(SensorDescriptor descriptor) {
for (String repository : ErrorAwayRulesMapping.REPOSITORIES) {
descriptor.createIssuesForRuleRepository(repository);
}
descriptor.createIssuesForRuleRepositories(
ErrorAwayRulesMapping.REPOSITORIES.toArray(new String[0]));

descriptor.onlyOnLanguage("java");
descriptor.name("Errorprone sensor");
Expand Down
Loading