diff --git a/README.md b/README.md index ad77b8a..326104f 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/pom.xml b/pom.xml index fb9df45..5ad2535 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 5.6.0.2578 2.42.0 - 0.12.7 + 0.12.15 0.1.29 0.25.0 diff --git a/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java b/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java index cd372f7..f14065c 100644 --- a/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java +++ b/sonar-erroraway-lib/src/main/java/com/github/erroraway/rules/ErrorAwayRulesMapping.java @@ -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; diff --git a/sonar-erroraway-sonar-plugin/pom.xml b/sonar-erroraway-sonar-plugin/pom.xml index 54de820..ea40894 100644 --- a/sonar-erroraway-sonar-plugin/pom.xml +++ b/sonar-erroraway-sonar-plugin/pom.xml @@ -236,7 +236,7 @@ org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin - 1.21.0.505 + 1.23.0.740 true errorawaysonar diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java index 005b92f..41005b9 100644 --- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java +++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayDiagnosticListener.java @@ -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; @@ -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 @@ -44,6 +50,8 @@ public class ErrorAwayDiagnosticListener implements DiagnosticListener { private static final Logger LOGGER = LoggerFactory.getLogger(ErrorAwayDiagnosticListener.class); + private static final Map PLUGIN_RULE_REPOSITORY_MAP = buildPluginRuleRepositoryMap(); + public static final String ERROR_PRONE_COMPILER_CRASH_CODE = "compiler.err.error.prone.crash"; private static final Set ERROR_PRONE_DIAGNOSTIC_CODES = Set.of("compiler.warn.error.prone", "compiler.err.error.prone", @@ -62,7 +70,7 @@ public void report(Diagnostic 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(); @@ -156,18 +164,20 @@ private String parseRule(Diagnostic 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 buildPluginRuleRepositoryMap() { + Map map = new HashMap<>(); + Iterator 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); } } \ No newline at end of file diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java index 62f78cd..41d9821 100644 --- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java +++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwayPlugin.java @@ -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); diff --git a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java index 5848ae7..b822dba 100644 --- a/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java +++ b/sonar-erroraway-sonar-plugin/src/main/java/com/github/erroraway/sonarqube/ErrorAwaySensor.java @@ -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; @@ -92,7 +93,7 @@ public void execute(SensorContext context) { } // Compiler options - ErrorProneOptions errorProneOptions = buildErrorProneOptions(context); + ErrorProneOptions errorProneOptions = buildErrorProneOptions(context, checkers); List javacOptions = buildJavacOptions(); // Setup the compiler and analyze the code @@ -121,14 +122,16 @@ public void execute(SensorContext context) { } } - private ErrorProneOptions buildErrorProneOptions(SensorContext context) { + private ErrorProneOptions buildErrorProneOptions(SensorContext context, List> checkers) { Configuration configuration = context.config(); List 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 @@ -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 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 buildJavacOptions() { List options = new ArrayList<>(); @@ -220,9 +273,8 @@ private void addErrorProneCheckers(SensorContext context, List