diff --git a/docs/semantic-model-telemetry.md b/docs/semantic-model-telemetry.md new file mode 100644 index 00000000000..21e82745cea --- /dev/null +++ b/docs/semantic-model-telemetry.md @@ -0,0 +1,43 @@ +# Java semantic model telemetry + +These counters describe observed semantic degradation and possible configuration causes. They do not classify a project as misconfigured automatically. All counters aggregate across modules and contain no source paths, type names, or diagnostic messages. + +## Semantic observations + +The prefix `java.analysis..semantic.` applies separately to `main`, `test`, and `generated` sources. + +| Suffix | Meaning | +| --- | --- | +| `files_evaluated` | Files for which parsing and AST conversion returned a semantic model and its diagnostics were examined. | +| `files_with_resolution_errors` | Evaluated files with at least one `UNDEFINED_TYPE` diagnostic. | +| `resolution_error_count` | Sum of `UNDEFINED_TYPE` diagnostics, deduplicated by message within each file. Preview-feature diagnostics are excluded. | +| `modules_evaluated` | Modules with at least one evaluated file in this scope. | +| `modules_with_resolution_errors` | Modules with at least one affected file in this scope. | + +Observations are recorded before rule execution, so a later visitor failure does not erase the semantic observation. The existing `success.type_error_count` remains unchanged, including its preview-feature diagnostics and restriction to successful analysis. + +The affected-file ratio is `files_with_resolution_errors / files_evaluated`. An absent or zero denominator means unobserved. Zero resolution errors means no degradation was detected by these diagnostics; it does not prove that every binding resolved correctly. No explicit model-absence counter is emitted because normal Java parsing constructs a semantic model even with an incomplete classpath. + +## Analysis coverage + +Under `java.analysis..`, the counters `success.file_count`, `parse_errors.file_count`, and `exceptions.file_count` count file processing outcomes, alongside the existing size and time measurements. `cached_file_count` applies only to `main` and `test`: it counts files successfully processed without parsing through server-side caching and is emitted when that optimization is attempted. Generated sources always go through parsing and do not emit this counter. Cached files have no new semantic observation. A parse failure has no semantic observation either. An exception may occur before or after semantic observation, so these counters must not be added to `files_evaluated` as disjoint populations. + +## Configuration signals + +The prefix `java.classpath..` applies separately to `main` and `test`. Only modules containing Java sources in the corresponding scope contribute. + +| Suffix | Meaning | +| --- | --- | +| `module_count` | Modules whose classpath configuration was examined. | +| `modules_missing_binaries` | Modules without the corresponding binaries property. | +| `modules_without_binary_entries` | Modules whose binaries property resolves to no entries. | +| `modules_missing_libraries` | Modules without the corresponding libraries property. | +| `modules_without_library_entries` | Modules whose libraries property resolves to no entries, excluding JDK entries. | +| `modules_with_unmatched_entries` | Modules with a configured binaries or libraries pattern that produces the existing unmatched-entry warning, including partial classpaths with other valid entries. | +| `modules_with_invalid_jdk` | Modules whose configured `sonar.java.jdkHome` path is missing or is not a directory, causing fallback to the runtime JDK. | + +These signals are collected for AutoScan too; existing warning suppression is unchanged. Missing libraries may be legitimate for JDK-only code. A resolved directory is an entry even if empty; these metrics do not inspect bytecode contents or establish freshness. Main and test configuration counters overlap when both scopes exist, so their module counts must not be summed to obtain distinct project modules. + +Use affected-file and affected-module ratios to measure degradation, then correlate with configuration signals and existing scanner, AutoScan, language-version, and analyzer-version context. Project aggregates cannot prove that a configuration problem and a resolution error occurred in the same module. Track distinct projects using their latest analysis and distinguish full observations from partial or cache-only observations. + +Invalid classpath entries that abort analysis, binding-level completeness, diagnostic categories beyond the existing parser diagnostics, and telemetry for failed scanner executions are outside this first iteration. The existing project-end sensor submits these counters on completed analyses. diff --git a/its/plugin/tests/src/test/java/com/sonar/it/java/JspTest.java b/its/plugin/tests/src/test/java/com/sonar/it/java/JspTest.java index 9bf03f37759..4c04865c121 100644 --- a/its/plugin/tests/src/test/java/com/sonar/it/java/JspTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/it/java/JspTest.java @@ -94,15 +94,47 @@ public void should_transpile_jsp() throws Exception { // size of the generated files varies depending on the environment line endings assertThat(extractTelemetryLogs(buildResult)) .matches(patternWithLiteralDot(""" + Telemetry java.analysis.generated.semantic.files_evaluated: 4 + Telemetry java.analysis.generated.semantic.files_with_resolution_errors: 0 + Telemetry java.analysis.generated.semantic.modules_evaluated: 1 + Telemetry java.analysis.generated.semantic.modules_with_resolution_errors: 0 + Telemetry java.analysis.generated.semantic.resolution_error_count: 0 + Telemetry java.analysis.generated.success.file_count: 4 Telemetry java.analysis.generated.success.size_chars: \\d{5} Telemetry java.analysis.generated.success.time_ms: \\d+ Telemetry java.analysis.generated.success.type_error_count: 0 + Telemetry java.analysis.main.semantic.files_evaluated: 1 + Telemetry java.analysis.main.semantic.files_with_resolution_errors: 0 + Telemetry java.analysis.main.semantic.modules_evaluated: 1 + Telemetry java.analysis.main.semantic.modules_with_resolution_errors: 0 + Telemetry java.analysis.main.semantic.resolution_error_count: 0 + Telemetry java.analysis.main.success.file_count: 1 Telemetry java.analysis.main.success.size_chars: 969 Telemetry java.analysis.main.success.time_ms: \\d+ Telemetry java.analysis.main.success.type_error_count: 0 + Telemetry java.analysis.test.semantic.files_evaluated: 1 + Telemetry java.analysis.test.semantic.files_with_resolution_errors: 0 + Telemetry java.analysis.test.semantic.modules_evaluated: 1 + Telemetry java.analysis.test.semantic.modules_with_resolution_errors: 0 + Telemetry java.analysis.test.semantic.resolution_error_count: 0 + Telemetry java.analysis.test.success.file_count: 1 Telemetry java.analysis.test.success.size_chars: 20 Telemetry java.analysis.test.success.time_ms: \\d+ Telemetry java.analysis.test.success.type_error_count: 0 + Telemetry java.classpath.main.module_count: 1 + Telemetry java.classpath.main.modules_missing_binaries: 0 + Telemetry java.classpath.main.modules_missing_libraries: 0 + Telemetry java.classpath.main.modules_with_invalid_jdk: 0 + Telemetry java.classpath.main.modules_with_unmatched_entries: 0 + Telemetry java.classpath.main.modules_without_binary_entries: 0 + Telemetry java.classpath.main.modules_without_library_entries: 0 + Telemetry java.classpath.test.module_count: 1 + Telemetry java.classpath.test.modules_missing_binaries: 0 + Telemetry java.classpath.test.modules_missing_libraries: 0 + Telemetry java.classpath.test.modules_with_invalid_jdk: 0 + Telemetry java.classpath.test.modules_with_unmatched_entries: 0 + Telemetry java.classpath.test.modules_without_binary_entries: 0 + Telemetry java.classpath.test.modules_without_library_entries: 0 Telemetry java.dependency.lombok: absent Telemetry java.dependency.spring-boot: absent Telemetry java.dependency.spring-web: absent diff --git a/its/plugin/tests/src/test/java/com/sonar/it/java/suite/JavaTutorialTest.java b/its/plugin/tests/src/test/java/com/sonar/it/java/suite/JavaTutorialTest.java index 6593d967518..84e48d662fa 100644 --- a/its/plugin/tests/src/test/java/com/sonar/it/java/suite/JavaTutorialTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/it/java/suite/JavaTutorialTest.java @@ -73,9 +73,22 @@ private void executeAndAssertBuild(MavenBuild build, String projectKey) { assertThat(extractTelemetryLogs(buildResult)) .matches(patternWithLiteralDot(""" + Telemetry java.analysis.main.semantic.files_evaluated: 8 + Telemetry java.analysis.main.semantic.files_with_resolution_errors: 0 + Telemetry java.analysis.main.semantic.modules_evaluated: 1 + Telemetry java.analysis.main.semantic.modules_with_resolution_errors: 0 + Telemetry java.analysis.main.semantic.resolution_error_count: 0 + Telemetry java.analysis.main.success.file_count: 8 Telemetry java.analysis.main.success.size_chars: \\d{4} Telemetry java.analysis.main.success.time_ms: \\d+ Telemetry java.analysis.main.success.type_error_count: 0 + Telemetry java.classpath.main.module_count: 1 + Telemetry java.classpath.main.modules_missing_binaries: 0 + Telemetry java.classpath.main.modules_missing_libraries: 0 + Telemetry java.classpath.main.modules_with_invalid_jdk: 0 + Telemetry java.classpath.main.modules_with_unmatched_entries: 0 + Telemetry java.classpath.main.modules_without_binary_entries: 0 + Telemetry java.classpath.main.modules_without_library_entries: 0 Telemetry java.dependency.lombok: absent Telemetry java.dependency.spring-boot: absent Telemetry java.dependency.spring-web: 5.3.18 diff --git a/its/plugin/tests/src/test/java/com/sonar/it/java/suite/MultiModuleTelemetryTest.java b/its/plugin/tests/src/test/java/com/sonar/it/java/suite/MultiModuleTelemetryTest.java index e0d228a35a6..f146c1beff9 100644 --- a/its/plugin/tests/src/test/java/com/sonar/it/java/suite/MultiModuleTelemetryTest.java +++ b/its/plugin/tests/src/test/java/com/sonar/it/java/suite/MultiModuleTelemetryTest.java @@ -44,6 +44,13 @@ public void test() { assertThat(buildResult.getLogs()) .containsOnlyOnce("Telemetry java.language.version: 8") .containsOnlyOnce("Telemetry java.module_count: 2") + .containsOnlyOnce("Telemetry java.analysis.main.success.file_count: 2") + .containsOnlyOnce("Telemetry java.analysis.main.semantic.files_evaluated: 2") + .containsOnlyOnce("Telemetry java.analysis.main.semantic.modules_evaluated: 2") + .containsOnlyOnce("Telemetry java.analysis.test.semantic.files_evaluated: 2") + .containsOnlyOnce("Telemetry java.analysis.test.semantic.modules_evaluated: 2") + .containsOnlyOnce("Telemetry java.classpath.main.module_count: 2") + .containsOnlyOnce("Telemetry java.classpath.test.module_count: 2") .containsOnlyOnce("Telemetry java.scanner_app: ScannerMaven") .containsOnlyOnce("Telemetry java.is_autoscan: false") .containsOnlyOnce("Telemetry java.dependency.lombok: 1.18.30,1.18.38"); diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java index 09c44fc0ac2..f670ca7596d 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFrontend.java @@ -107,6 +107,7 @@ public JavaFrontend(JavaVersion javaVersion, SonarComponents sonarComponents, Me testCodeVisitors.addAll(sonarComponents.testChecks()); List jspCodeVisitors = sonarComponents.jspChecks(); boolean inAndroidContext = sonarComponents.inAndroidContext(); + sonarComponents.recordClasspathTelemetry(telemetry); globalClasspath = Stream.of(classpath, testClasspath, jspClasspath) .flatMap(Collection::stream).distinct().toList(); diff --git a/java-frontend/src/main/java/org/sonar/java/SonarComponents.java b/java-frontend/src/main/java/org/sonar/java/SonarComponents.java index e57da7ea757..47f3d93ac95 100644 --- a/java-frontend/src/main/java/org/sonar/java/SonarComponents.java +++ b/java-frontend/src/main/java/org/sonar/java/SonarComponents.java @@ -68,6 +68,8 @@ import org.sonar.java.model.LineUtils; import org.sonar.java.reporting.AnalyzerMessage; import org.sonar.java.reporting.JavaIssue; +import org.sonar.java.telemetry.Telemetry; +import org.sonar.java.telemetry.TelemetryKey; import org.sonar.java.utils.ModuleMetadataUtils; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; @@ -252,6 +254,13 @@ public NewHighlighting highlightableFor(InputFile inputFile) { return context.newHighlighting().onFile(inputFile); } + public void recordClasspathTelemetry(Telemetry telemetry) { + if (javaClasspath != null) { + javaClasspath.recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + } + javaTestClasspath.recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_TEST); + } + public List getJavaClasspath() { if (javaClasspath == null) { return new ArrayList<>(); diff --git a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java index fcd39ab2a21..dcb31a2272e 100644 --- a/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java +++ b/java-frontend/src/main/java/org/sonar/java/ast/JavaAstScanner.java @@ -62,6 +62,8 @@ public class JavaAstScanner { private final Telemetry telemetry; private final JavaAnalysisKeys javaAnalysisKeys; private VisitorsBridge visitor; + private boolean semanticModelEvaluated; + private boolean resolutionErrorsDetected; private boolean reportedMisconfiguredVersion = false; public JavaAstScanner(@Nullable SonarComponents sonarComponents, Telemetry telemetry, JavaAnalysisKeys javaAnalysisKeys) { @@ -82,9 +84,10 @@ public List getClasspath() { * under the {@code false} key, files that need to be parsed for further analysis. */ public Map> scanWithoutParsing(Iterable inputFiles) { - return StreamSupport.stream(inputFiles.spliterator(), false) - // Split files between successfully scanned without parsing and failed to scan without parsing + Map> files = StreamSupport.stream(inputFiles.spliterator(), false) .collect(Collectors.partitioningBy(visitor::scanWithoutParsing)); + telemetry.aggregateAsCounter(javaAnalysisKeys.cachedFileCountKey(), files.get(true).size()); + return files; } public void scan(Iterable inputFiles) { @@ -163,6 +166,7 @@ public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consume var telemetryAnalysisKeys = javaAnalysisKeys.exceptions(); try { JavaTree.CompilationUnitTreeImpl ast = result.get(); + recordSemanticTelemetry(ast.sema.undefinedTypes()); modifyCompilationUnit.accept(ast); visitor.visitFile(ast, sonarComponents != null && sonarComponents.fileCanBeSkipped(inputFile)); String path = inputFile.toString(); @@ -189,11 +193,29 @@ public void simpleScan(InputFile inputFile, JParserConfig.Result result, Consume throw error; } } finally { + telemetry.aggregateAsCounter(telemetryAnalysisKeys.fileCountKey(), 1); telemetry.aggregateAsCounter(telemetryAnalysisKeys.sizeCharsKey(), InputFileUtils.charCount(inputFile, 0)); telemetry.aggregateAsCounter(telemetryAnalysisKeys.timeMsKey(), currentTimeMillis() - startTime); } } + private void recordSemanticTelemetry(Set problems) { + var keys = javaAnalysisKeys.semantic(); + long resolutionErrors = problems.stream().filter(problem -> problem.type() == JProblem.Type.UNDEFINED_TYPE).count(); + telemetry.aggregateAsCounter(keys.filesEvaluated(), 1); + telemetry.aggregateAsCounter(keys.filesWithResolutionErrors(), resolutionErrors > 0 ? 1 : 0); + telemetry.aggregateAsCounter(keys.resolutionErrorCount(), resolutionErrors); + if (!semanticModelEvaluated) { + telemetry.aggregateAsCounter(keys.modulesEvaluated(), 1); + telemetry.aggregateAsCounter(keys.modulesWithResolutionErrors(), 0); + semanticModelEvaluated = true; + } + if (resolutionErrors > 0 && !resolutionErrorsDetected) { + telemetry.aggregateAsCounter(keys.modulesWithResolutionErrors(), 1); + resolutionErrorsDetected = true; + } + } + private static void cleanUpAst(JavaTree.CompilationUnitTreeImpl ast) { // release environment used for semantic resolution ast.sema.getEnvironmentCleaner().run(); diff --git a/java-frontend/src/main/java/org/sonar/java/classpath/AbstractClasspath.java b/java-frontend/src/main/java/org/sonar/java/classpath/AbstractClasspath.java index 13025544445..7db05866e22 100644 --- a/java-frontend/src/main/java/org/sonar/java/classpath/AbstractClasspath.java +++ b/java-frontend/src/main/java/org/sonar/java/classpath/AbstractClasspath.java @@ -49,6 +49,8 @@ import org.sonar.api.config.Configuration; import org.sonar.java.AnalysisWarningsWrapper; import org.sonar.java.SonarComponents; +import org.sonar.java.telemetry.Telemetry; +import org.sonar.java.telemetry.TelemetryKey.ClasspathKeys; import org.sonarsource.api.sonarlint.SonarLintSide; @ScannerSide @@ -70,6 +72,9 @@ public abstract class AbstractClasspath { protected List elements; protected boolean validateLibraries; protected boolean initialized; + private int libraryEntryCount; + private boolean invalidJdkHome; + private boolean unmatchedClasspathEntries; private boolean inAndroidContext = false; private final Set classpathWarnings; protected final AnalysisWarningsWrapper analysisWarnings; @@ -96,6 +101,7 @@ protected void init() { binaries.addAll(getFilesFromProperty(binariesProperty)); Set libraries = new LinkedHashSet<>(getJdkJars()); Set extraLibraries = getFilesFromProperty(librariesProperty); + libraryEntryCount = extraLibraries.size(); libraries.addAll(extraLibraries); logResolvedFiles(binariesProperty, binaries); logResolvedFiles(librariesProperty, libraries); @@ -145,6 +151,7 @@ private Optional existingDirectoryOrLog(String path) { LOG.debug("Property '{}' set with: {}", ClasspathProperties.SONAR_JAVA_JDK_HOME, path); File file = new File(path); if (!file.exists() || !file.isDirectory()) { + invalidJdkHome = true; classpathWarnings.add("Invalid value '" + file.getAbsolutePath() + "' for '" + ClasspathProperties.SONAR_JAVA_JDK_HOME + "' property, defaulting to runtime JDK."); return Optional.empty(); } @@ -171,6 +178,7 @@ protected Set getFilesFromProperty(String property) { for (String pathPattern : fileNames) { Set libraryFilesForPattern = getFilesForPattern(baseDir.toPath(), pathPattern, isLibraryProperty); if (validateLibraries && libraryFilesForPattern.isEmpty() && hasJavaFiles) { + unmatchedClasspathEntries = true; classpathWarnings.add("Invalid value for '" + property + "', no files nor directories matching '" + pathPattern + "'."); } validateLibraries = validateLibs; @@ -371,6 +379,20 @@ private static Path resolvePath(Path baseDir, String fileName) { return filePath.normalize(); } + public void recordTelemetry(Telemetry telemetry, ClasspathKeys keys) { + if (!hasJavaFiles()) { + return; + } + init(); + telemetry.aggregateAsCounter(keys.moduleCount(), 1); + telemetry.aggregateAsCounter(keys.modulesMissingBinaries(), settings.hasKey(binariesProperty) ? 0 : 1); + telemetry.aggregateAsCounter(keys.modulesWithoutBinaryEntries(), binaries.isEmpty() ? 1 : 0); + telemetry.aggregateAsCounter(keys.modulesMissingLibraries(), settings.hasKey(librariesProperty) ? 0 : 1); + telemetry.aggregateAsCounter(keys.modulesWithoutLibraryEntries(), libraryEntryCount == 0 ? 1 : 0); + telemetry.aggregateAsCounter(keys.modulesWithInvalidJdk(), invalidJdkHome ? 1 : 0); + telemetry.aggregateAsCounter(keys.modulesWithUnmatchedEntries(), unmatchedClasspathEntries ? 1 : 0); + } + public List getElements() { init(); return elements; diff --git a/java-frontend/src/main/java/org/sonar/java/telemetry/TelemetryKey.java b/java-frontend/src/main/java/org/sonar/java/telemetry/TelemetryKey.java index 53c67f4d5a3..2fa82c1b421 100644 --- a/java-frontend/src/main/java/org/sonar/java/telemetry/TelemetryKey.java +++ b/java-frontend/src/main/java/org/sonar/java/telemetry/TelemetryKey.java @@ -44,6 +44,48 @@ public enum TelemetryKey { JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_TIME_MS("java.analysis.generated.parse_errors.time_ms"), JAVA_ANALYSIS_GENERATED_EXCEPTIONS_SIZE_CHARS("java.analysis.generated.exceptions.size_chars"), JAVA_ANALYSIS_GENERATED_EXCEPTIONS_TIME_MS("java.analysis.generated.exceptions.time_ms"), + JAVA_ANALYSIS_MAIN_SUCCESS_FILE_COUNT("java.analysis.main.success.file_count"), + JAVA_ANALYSIS_MAIN_PARSE_ERRORS_FILE_COUNT("java.analysis.main.parse_errors.file_count"), + JAVA_ANALYSIS_MAIN_EXCEPTIONS_FILE_COUNT("java.analysis.main.exceptions.file_count"), + JAVA_ANALYSIS_MAIN_CACHED_FILE_COUNT("java.analysis.main.cached_file_count"), + JAVA_ANALYSIS_MAIN_SEMANTIC_FILES_EVALUATED("java.analysis.main.semantic.files_evaluated"), + JAVA_ANALYSIS_MAIN_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS("java.analysis.main.semantic.files_with_resolution_errors"), + JAVA_ANALYSIS_MAIN_SEMANTIC_RESOLUTION_ERROR_COUNT("java.analysis.main.semantic.resolution_error_count"), + JAVA_ANALYSIS_MAIN_SEMANTIC_MODULES_EVALUATED("java.analysis.main.semantic.modules_evaluated"), + JAVA_ANALYSIS_MAIN_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS("java.analysis.main.semantic.modules_with_resolution_errors"), + JAVA_ANALYSIS_TEST_SUCCESS_FILE_COUNT("java.analysis.test.success.file_count"), + JAVA_ANALYSIS_TEST_PARSE_ERRORS_FILE_COUNT("java.analysis.test.parse_errors.file_count"), + JAVA_ANALYSIS_TEST_EXCEPTIONS_FILE_COUNT("java.analysis.test.exceptions.file_count"), + JAVA_ANALYSIS_TEST_CACHED_FILE_COUNT("java.analysis.test.cached_file_count"), + JAVA_ANALYSIS_TEST_SEMANTIC_FILES_EVALUATED("java.analysis.test.semantic.files_evaluated"), + JAVA_ANALYSIS_TEST_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS("java.analysis.test.semantic.files_with_resolution_errors"), + JAVA_ANALYSIS_TEST_SEMANTIC_RESOLUTION_ERROR_COUNT("java.analysis.test.semantic.resolution_error_count"), + JAVA_ANALYSIS_TEST_SEMANTIC_MODULES_EVALUATED("java.analysis.test.semantic.modules_evaluated"), + JAVA_ANALYSIS_TEST_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS("java.analysis.test.semantic.modules_with_resolution_errors"), + JAVA_ANALYSIS_GENERATED_SUCCESS_FILE_COUNT("java.analysis.generated.success.file_count"), + JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_FILE_COUNT("java.analysis.generated.parse_errors.file_count"), + JAVA_ANALYSIS_GENERATED_EXCEPTIONS_FILE_COUNT("java.analysis.generated.exceptions.file_count"), + JAVA_ANALYSIS_GENERATED_CACHED_FILE_COUNT("java.analysis.generated.cached_file_count"), + JAVA_ANALYSIS_GENERATED_SEMANTIC_FILES_EVALUATED("java.analysis.generated.semantic.files_evaluated"), + JAVA_ANALYSIS_GENERATED_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS("java.analysis.generated.semantic.files_with_resolution_errors"), + JAVA_ANALYSIS_GENERATED_SEMANTIC_RESOLUTION_ERROR_COUNT("java.analysis.generated.semantic.resolution_error_count"), + JAVA_ANALYSIS_GENERATED_SEMANTIC_MODULES_EVALUATED("java.analysis.generated.semantic.modules_evaluated"), + JAVA_ANALYSIS_GENERATED_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS("java.analysis.generated.semantic.modules_with_resolution_errors"), + JAVA_CLASSPATH_MAIN_MODULE_COUNT("java.classpath.main.module_count"), + JAVA_CLASSPATH_MAIN_MODULES_MISSING_BINARIES("java.classpath.main.modules_missing_binaries"), + JAVA_CLASSPATH_MAIN_MODULES_WITHOUT_BINARY_ENTRIES("java.classpath.main.modules_without_binary_entries"), + JAVA_CLASSPATH_MAIN_MODULES_MISSING_LIBRARIES("java.classpath.main.modules_missing_libraries"), + JAVA_CLASSPATH_MAIN_MODULES_WITHOUT_LIBRARY_ENTRIES("java.classpath.main.modules_without_library_entries"), + JAVA_CLASSPATH_MAIN_MODULES_WITH_INVALID_JDK("java.classpath.main.modules_with_invalid_jdk"), + JAVA_CLASSPATH_MAIN_MODULES_WITH_UNMATCHED_ENTRIES("java.classpath.main.modules_with_unmatched_entries"), + JAVA_CLASSPATH_TEST_MODULE_COUNT("java.classpath.test.module_count"), + JAVA_CLASSPATH_TEST_MODULES_MISSING_BINARIES("java.classpath.test.modules_missing_binaries"), + JAVA_CLASSPATH_TEST_MODULES_WITHOUT_BINARY_ENTRIES("java.classpath.test.modules_without_binary_entries"), + JAVA_CLASSPATH_TEST_MODULES_MISSING_LIBRARIES("java.classpath.test.modules_missing_libraries"), + JAVA_CLASSPATH_TEST_MODULES_WITHOUT_LIBRARY_ENTRIES("java.classpath.test.modules_without_library_entries"), + JAVA_CLASSPATH_TEST_MODULES_WITH_INVALID_JDK("java.classpath.test.modules_with_invalid_jdk"), + JAVA_CLASSPATH_TEST_MODULES_WITH_UNMATCHED_ENTRIES("java.classpath.test.modules_with_unmatched_entries"), + JAVA_MODULE_COUNT("java.module_count"), // The last element of dependency keys should be the same as the name of its jar. @@ -67,34 +109,75 @@ public interface SpeedKeys { TelemetryKey sizeCharsKey(); TelemetryKey timeMsKey(); + + TelemetryKey fileCountKey(); + } + + public record SizeAndTimeKeys(TelemetryKey sizeCharsKey, TelemetryKey timeMsKey, TelemetryKey fileCountKey) implements SpeedKeys { } - public record SizeAndTimeKeys(TelemetryKey sizeCharsKey, TelemetryKey timeMsKey) implements SpeedKeys { + public record SizeTimeAndTypeErrorKeys(TelemetryKey sizeCharsKey, TelemetryKey timeMsKey, TelemetryKey typeErrorCountKey, TelemetryKey fileCountKey) implements SpeedKeys { } - public record SizeTimeAndTypeErrorKeys(TelemetryKey sizeCharsKey, TelemetryKey timeMsKey, TelemetryKey typeErrorCountKey) implements SpeedKeys { + public record JavaAnalysisKeys(SizeTimeAndTypeErrorKeys success, SpeedKeys parseErrors, SpeedKeys exceptions, + TelemetryKey cachedFileCountKey, SemanticKeys semantic) { } - public record JavaAnalysisKeys(SizeTimeAndTypeErrorKeys success, SpeedKeys parseErrors, SpeedKeys exceptions) { + public record SemanticKeys(TelemetryKey filesEvaluated, TelemetryKey filesWithResolutionErrors, TelemetryKey resolutionErrorCount, + TelemetryKey modulesEvaluated, TelemetryKey modulesWithResolutionErrors) { + } + + public record ClasspathKeys(TelemetryKey moduleCount, TelemetryKey modulesMissingBinaries, TelemetryKey modulesWithoutBinaryEntries, + TelemetryKey modulesMissingLibraries, TelemetryKey modulesWithoutLibraryEntries, TelemetryKey modulesWithInvalidJdk, TelemetryKey modulesWithUnmatchedEntries) { } public static final JavaAnalysisKeys JAVA_ANALYSIS_MAIN = new JavaAnalysisKeys( new SizeTimeAndTypeErrorKeys(JAVA_ANALYSIS_MAIN_SUCCESS_SIZE_CHARS, JAVA_ANALYSIS_MAIN_SUCCESS_TIME_MS, - JAVA_ANALYSIS_MAIN_SUCCESS_TYPE_ERROR_COUNT), - new SizeAndTimeKeys(JAVA_ANALYSIS_MAIN_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_MAIN_PARSE_ERRORS_TIME_MS), - new SizeAndTimeKeys(JAVA_ANALYSIS_MAIN_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_MAIN_EXCEPTIONS_TIME_MS)); + JAVA_ANALYSIS_MAIN_SUCCESS_TYPE_ERROR_COUNT, JAVA_ANALYSIS_MAIN_SUCCESS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_MAIN_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_MAIN_PARSE_ERRORS_TIME_MS, + JAVA_ANALYSIS_MAIN_PARSE_ERRORS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_MAIN_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_MAIN_EXCEPTIONS_TIME_MS, + JAVA_ANALYSIS_MAIN_EXCEPTIONS_FILE_COUNT), + JAVA_ANALYSIS_MAIN_CACHED_FILE_COUNT, + new SemanticKeys(JAVA_ANALYSIS_MAIN_SEMANTIC_FILES_EVALUATED, JAVA_ANALYSIS_MAIN_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS, + JAVA_ANALYSIS_MAIN_SEMANTIC_RESOLUTION_ERROR_COUNT, JAVA_ANALYSIS_MAIN_SEMANTIC_MODULES_EVALUATED, + JAVA_ANALYSIS_MAIN_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS)); public static final JavaAnalysisKeys JAVA_ANALYSIS_TEST = new JavaAnalysisKeys( new SizeTimeAndTypeErrorKeys(JAVA_ANALYSIS_TEST_SUCCESS_SIZE_CHARS, JAVA_ANALYSIS_TEST_SUCCESS_TIME_MS, - JAVA_ANALYSIS_TEST_SUCCESS_TYPE_ERROR_COUNT), - new SizeAndTimeKeys(JAVA_ANALYSIS_TEST_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_TEST_PARSE_ERRORS_TIME_MS), - new SizeAndTimeKeys(JAVA_ANALYSIS_TEST_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_TEST_EXCEPTIONS_TIME_MS)); + JAVA_ANALYSIS_TEST_SUCCESS_TYPE_ERROR_COUNT, JAVA_ANALYSIS_TEST_SUCCESS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_TEST_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_TEST_PARSE_ERRORS_TIME_MS, + JAVA_ANALYSIS_TEST_PARSE_ERRORS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_TEST_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_TEST_EXCEPTIONS_TIME_MS, + JAVA_ANALYSIS_TEST_EXCEPTIONS_FILE_COUNT), + JAVA_ANALYSIS_TEST_CACHED_FILE_COUNT, + new SemanticKeys(JAVA_ANALYSIS_TEST_SEMANTIC_FILES_EVALUATED, JAVA_ANALYSIS_TEST_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS, + JAVA_ANALYSIS_TEST_SEMANTIC_RESOLUTION_ERROR_COUNT, JAVA_ANALYSIS_TEST_SEMANTIC_MODULES_EVALUATED, + JAVA_ANALYSIS_TEST_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS)); public static final JavaAnalysisKeys JAVA_ANALYSIS_GENERATED = new JavaAnalysisKeys( new SizeTimeAndTypeErrorKeys(JAVA_ANALYSIS_GENERATED_SUCCESS_SIZE_CHARS, JAVA_ANALYSIS_GENERATED_SUCCESS_TIME_MS, - JAVA_ANALYSIS_GENERATED_SUCCESS_TYPE_ERROR_COUNT), - new SizeAndTimeKeys(JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_TIME_MS), - new SizeAndTimeKeys(JAVA_ANALYSIS_GENERATED_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_GENERATED_EXCEPTIONS_TIME_MS)); + JAVA_ANALYSIS_GENERATED_SUCCESS_TYPE_ERROR_COUNT, JAVA_ANALYSIS_GENERATED_SUCCESS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_SIZE_CHARS, JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_TIME_MS, + JAVA_ANALYSIS_GENERATED_PARSE_ERRORS_FILE_COUNT), + new SizeAndTimeKeys(JAVA_ANALYSIS_GENERATED_EXCEPTIONS_SIZE_CHARS, JAVA_ANALYSIS_GENERATED_EXCEPTIONS_TIME_MS, + JAVA_ANALYSIS_GENERATED_EXCEPTIONS_FILE_COUNT), + JAVA_ANALYSIS_GENERATED_CACHED_FILE_COUNT, + new SemanticKeys(JAVA_ANALYSIS_GENERATED_SEMANTIC_FILES_EVALUATED, JAVA_ANALYSIS_GENERATED_SEMANTIC_FILES_WITH_RESOLUTION_ERRORS, + JAVA_ANALYSIS_GENERATED_SEMANTIC_RESOLUTION_ERROR_COUNT, JAVA_ANALYSIS_GENERATED_SEMANTIC_MODULES_EVALUATED, + JAVA_ANALYSIS_GENERATED_SEMANTIC_MODULES_WITH_RESOLUTION_ERRORS)); + + public static final ClasspathKeys JAVA_CLASSPATH_MAIN = new ClasspathKeys( + JAVA_CLASSPATH_MAIN_MODULE_COUNT, JAVA_CLASSPATH_MAIN_MODULES_MISSING_BINARIES, + JAVA_CLASSPATH_MAIN_MODULES_WITHOUT_BINARY_ENTRIES, JAVA_CLASSPATH_MAIN_MODULES_MISSING_LIBRARIES, + JAVA_CLASSPATH_MAIN_MODULES_WITHOUT_LIBRARY_ENTRIES, JAVA_CLASSPATH_MAIN_MODULES_WITH_INVALID_JDK, + JAVA_CLASSPATH_MAIN_MODULES_WITH_UNMATCHED_ENTRIES); + + public static final ClasspathKeys JAVA_CLASSPATH_TEST = new ClasspathKeys( + JAVA_CLASSPATH_TEST_MODULE_COUNT, JAVA_CLASSPATH_TEST_MODULES_MISSING_BINARIES, + JAVA_CLASSPATH_TEST_MODULES_WITHOUT_BINARY_ENTRIES, JAVA_CLASSPATH_TEST_MODULES_MISSING_LIBRARIES, + JAVA_CLASSPATH_TEST_MODULES_WITHOUT_LIBRARY_ENTRIES, JAVA_CLASSPATH_TEST_MODULES_WITH_INVALID_JDK, + JAVA_CLASSPATH_TEST_MODULES_WITH_UNMATCHED_ENTRIES); private final String key; diff --git a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java index 9c7d66bc68e..3a820c09c75 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/JavaAstScannerTest.java @@ -36,6 +36,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.event.Level; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.issue.NoSonarFilter; import org.sonar.api.testfixtures.log.LogTesterJUnit5; import org.sonar.java.AnalysisException; @@ -55,6 +56,7 @@ import org.sonar.java.model.declaration.ClassTreeImpl; import org.sonar.java.model.expression.IdentifierTreeImpl; import org.sonar.java.notchecks.VisitorNotInChecksPackage; +import org.sonar.java.telemetry.DefaultTelemetry; import org.sonar.java.telemetry.NoOpTelemetry; import org.sonar.java.telemetry.TelemetryKey; import org.sonar.java.testing.ThreadLocalLogTester; @@ -71,6 +73,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -385,10 +388,12 @@ void should_report_analysis_error_in_sonarLint_context_withSQ_6_0() { } @Test - void skippableVisitors_are_not_used_when_file_is_unchanged() throws ApiMismatchException { + void skippableVisitors_are_not_used_when_file_is_unchanged() throws Exception { SonarComponents sonarComponents = mock(SonarComponents.class); doReturn(true).when(sonarComponents).canSkipUnchangedFiles(); doReturn(true).when(sonarComponents).fileCanBeSkipped(any()); + var symbolTable = mock(NewSymbolTable.class, RETURNS_DEEP_STUBS); + when(sonarComponents.symbolizableFor(any())).thenReturn(symbolTable); JavaAstScanner jas = new JavaAstScanner(sonarComponents, new NoOpTelemetry(), TelemetryKey.JAVA_ANALYSIS_MAIN); VisitorThatCanBeSkipped skippable = spy(new VisitorThatCanBeSkipped()); @@ -401,7 +406,9 @@ void skippableVisitors_are_not_used_when_file_is_unchanged() throws ApiMismatchE jas.setVisitorBridge(visitorsBridge); InputFile inputFile = new TestInputFileBuilder("class A {}", "file.java").build(); - jas.simpleScan(inputFile, mock(JParserConfig.Result.class), ignored -> {}); + var result = mock(JParserConfig.Result.class); + when(result.get()).thenReturn((JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse("class A {}")); + jas.simpleScan(inputFile, result, ignored -> {}); verify(skippable, never()).visitNode(any()); verify(unskippable, times(1)).visitNode(any()); @@ -439,13 +446,15 @@ void scanWithoutParsing_filters_out_the_files_that_could_be_successfully_scanned doReturn(true).when(visitorsBridge).scanWithoutParsing(any()); doReturn(false).when(visitorsBridge).scanWithoutParsing(unsuccessful); - JavaAstScanner javaAstScanner = new JavaAstScanner(mock(SonarComponents.class), new NoOpTelemetry(), TelemetryKey.JAVA_ANALYSIS_MAIN); + var telemetry = new DefaultTelemetry(); + JavaAstScanner javaAstScanner = new JavaAstScanner(mock(SonarComponents.class), telemetry, TelemetryKey.JAVA_ANALYSIS_MAIN); javaAstScanner.setVisitorBridge(visitorsBridge); Map> actual = javaAstScanner.scanWithoutParsing(files); assertThat(actual).hasSize(2); assertThat(actual.get(false)).containsExactly(unsuccessful); assertThat(actual.get(true)).containsExactly(successful, successful); + assertThat(telemetry.toMap()).containsExactlyEntriesOf(Map.of("java.analysis.main.cached_file_count", "2")); } @Test diff --git a/java-frontend/src/test/java/org/sonar/java/classpath/ClasspathForMainTest.java b/java-frontend/src/test/java/org/sonar/java/classpath/ClasspathForMainTest.java index 44aecd375d1..509ce8a2f19 100644 --- a/java-frontend/src/test/java/org/sonar/java/classpath/ClasspathForMainTest.java +++ b/java-frontend/src/test/java/org/sonar/java/classpath/ClasspathForMainTest.java @@ -35,6 +35,8 @@ import org.sonar.java.AnalysisWarningsWrapper; import org.sonar.java.SonarComponents; import org.sonar.java.TestUtils; +import org.sonar.java.telemetry.DefaultTelemetry; +import org.sonar.java.telemetry.TelemetryKey; import org.sonar.java.testing.ThreadLocalLogTester; import org.sonar.scanner.extension.PropertyDefinitions; import org.sonar.scanner.plugin.api.impl.config.MapSettings; @@ -98,6 +100,79 @@ void no_interaction_with_FileSystem_at_initialization() { assertThat(analysisWarningsLogged).isEmpty(); } + @Test + void telemetry_distinguishes_missing_configuration_from_empty_entries() { + settings.removeProperty(ClasspathProperties.SONAR_JAVA_BINARIES); + settings.removeProperty(ClasspathProperties.SONAR_JAVA_LIBRARIES); + var telemetry = new DefaultTelemetry(); + createJavaClasspath().recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + assertThat(telemetry.toMap()) + .containsEntry("java.classpath.main.module_count", "1") + .containsEntry("java.classpath.main.modules_missing_binaries", "1") + .containsEntry("java.classpath.main.modules_without_binary_entries", "1") + .containsEntry("java.classpath.main.modules_missing_libraries", "1") + .containsEntry("java.classpath.main.modules_without_library_entries", "1") + .containsEntry("java.classpath.main.modules_with_invalid_jdk", "0"); + + settings.setProperty(ClasspathProperties.SONAR_JAVA_BINARIES, "empty"); + settings.setProperty(ClasspathProperties.SONAR_JAVA_LIBRARIES, "empty"); + settings.setProperty(ClasspathProperties.SONAR_JAVA_JDK_HOME, "does-not-exist"); + createJavaClasspath().recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + assertThat(telemetry.toMap()) + .containsEntry("java.classpath.main.module_count", "2") + .containsEntry("java.classpath.main.modules_missing_binaries", "1") + .containsEntry("java.classpath.main.modules_without_binary_entries", "1") + .containsEntry("java.classpath.main.modules_missing_libraries", "1") + .containsEntry("java.classpath.main.modules_without_library_entries", "1") + .containsEntry("java.classpath.main.modules_with_invalid_jdk", "1"); + } + + @Test + void telemetry_detects_unmatched_patterns_alongside_valid_entries() { + settings.setProperty(ClasspathProperties.SONAR_JAVA_LIBRARIES, "empty,does-not-exist/*.jar"); + var classpath = createJavaClasspath(); + classpath.getElements(); + classpath.logClasspathWarnings(); + var telemetry = new DefaultTelemetry(); + classpath.recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + assertThat(telemetry.toMap()) + .containsEntry("java.classpath.main.modules_with_unmatched_entries", "1") + .containsEntry("java.classpath.main.modules_without_library_entries", "0") + .containsEntry("java.classpath.main.modules_with_invalid_jdk", "0"); + } + + @Test + void telemetry_excludes_jdk_entries_from_dependency_library_count() { + settings.removeProperty(ClasspathProperties.SONAR_JAVA_LIBRARIES); + settings.setProperty(ClasspathProperties.SONAR_JAVA_JDK_HOME, jdkPath.toString()); + var telemetry = new DefaultTelemetry(); + createJavaClasspath().recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + assertThat(telemetry.toMap()) + .containsEntry("java.classpath.main.modules_without_library_entries", "1") + .containsEntry("java.classpath.main.modules_with_invalid_jdk", "0"); + } + + @Test + void telemetry_ignores_scopes_without_java_sources() { + fs = new DefaultFileSystem(baseDir); + var telemetry = new DefaultTelemetry(); + createJavaClasspath().recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_MAIN); + assertThat(telemetry.toMap()).isEmpty(); + } + + @Test + void telemetry_records_test_classpath_separately() { + fs = new DefaultFileSystem(baseDir); + fs.add(TestUtils.emptyInputFile("Test.java", InputFile.Type.TEST)); + var telemetry = new DefaultTelemetry(); + new ClasspathForTest(settings.asConfig(), fs).recordTelemetry(telemetry, TelemetryKey.JAVA_CLASSPATH_TEST); + assertThat(telemetry.toMap()) + .containsEntry("java.classpath.test.module_count", "1") + .containsEntry("java.classpath.test.modules_missing_binaries", "1") + .containsEntry("java.classpath.test.modules_missing_libraries", "1") + .doesNotContainKey("java.classpath.main.module_count"); + } + @Test void properties() { assertThat(ClasspathProperties.getProperties()).hasSize(5); diff --git a/java-frontend/src/test/java/org/sonar/java/model/SemanticTelemetryTest.java b/java-frontend/src/test/java/org/sonar/java/model/SemanticTelemetryTest.java new file mode 100644 index 00000000000..9968d98f826 --- /dev/null +++ b/java-frontend/src/test/java/org/sonar/java/model/SemanticTelemetryTest.java @@ -0,0 +1,135 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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.model; + +import com.sonar.sslr.api.RecognitionException; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.sonar.java.TestUtils; +import org.sonar.java.ast.JavaAstScanner; +import org.sonar.java.telemetry.DefaultTelemetry; +import org.sonar.java.telemetry.TelemetryKey; +import org.sonar.java.telemetry.TelemetryKey.JavaAnalysisKeys; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SemanticTelemetryTest { + + private final DefaultTelemetry telemetry = new DefaultTelemetry(); + + static Stream scopes() { + return Stream.of(TelemetryKey.JAVA_ANALYSIS_MAIN, TelemetryKey.JAVA_ANALYSIS_TEST, TelemetryKey.JAVA_ANALYSIS_GENERATED); + } + + @ParameterizedTest + @MethodSource("scopes") + void counts_affected_files_and_modules_once(JavaAnalysisKeys keys) throws Exception { + JavaAstScanner scanner = scanner(keys); + scanSource(scanner, "class A { String value; }"); + scanSource(scanner, "class A { Missing a; Missing b; Other c; }"); + scanSource(scanner, "class A { Missing a; Missing b; Other c; }"); + + var semantic = keys.semantic(); + assertThat(telemetry.toMap()) + .containsEntry(semantic.filesEvaluated().key(), "3") + .containsEntry(semantic.filesWithResolutionErrors().key(), "2") + .containsEntry(semantic.resolutionErrorCount().key(), "4") + .containsEntry(semantic.modulesEvaluated().key(), "1") + .containsEntry(semantic.modulesWithResolutionErrors().key(), "1") + .containsEntry(keys.success().fileCountKey().key(), "3"); + + scanSource(scanner(keys), "class A { String value; }"); + assertThat(telemetry.toMap()) + .containsEntry(semantic.modulesEvaluated().key(), "2") + .containsEntry(semantic.modulesWithResolutionErrors().key(), "1"); + } + + @Test + void preview_diagnostics_are_excluded_from_resolution_errors() throws Exception { + var tree = mock(JavaTree.CompilationUnitTreeImpl.class); + tree.sema = mock(JSema.class); + when(tree.sema.undefinedTypes()).thenReturn(Set.of(new JProblem("Preview feature", JProblem.Type.PREVIEW_FEATURE_USED))); + var result = mock(JParserConfig.Result.class); + when(result.get()).thenReturn(tree); + + scanner(TelemetryKey.JAVA_ANALYSIS_MAIN).simpleScan(TestUtils.emptyInputFile("A.java"), result, ignored -> {}); + + assertThat(telemetry.toMap()) + .containsEntry("java.analysis.main.semantic.files_evaluated", "1") + .containsEntry("java.analysis.main.semantic.files_with_resolution_errors", "0") + .containsEntry("java.analysis.main.semantic.resolution_error_count", "0") + .containsEntry("java.analysis.main.semantic.modules_with_resolution_errors", "0") + .containsEntry("java.analysis.main.success.type_error_count", "1"); + } + + @Test + void failed_parsing_is_unobserved() throws Exception { + var result = mock(JParserConfig.Result.class); + when(result.get()).thenThrow(new RecognitionException(-1, "Invalid syntax")); + scanner(TelemetryKey.JAVA_ANALYSIS_MAIN).simpleScan(TestUtils.emptyInputFile("A.java"), result, ignored -> {}); + + assertThat(telemetry.toMap()) + .containsEntry("java.analysis.main.parse_errors.file_count", "1") + .doesNotContainKey("java.analysis.main.semantic.files_evaluated"); + } + + @Test + void parser_failure_is_unobserved() throws Exception { + var result = mock(JParserConfig.Result.class); + when(result.get()).thenThrow(new IllegalStateException("Parser failure")); + scanner(TelemetryKey.JAVA_ANALYSIS_MAIN).simpleScan(TestUtils.emptyInputFile("A.java"), result, ignored -> {}); + + assertThat(telemetry.toMap()) + .containsEntry("java.analysis.main.exceptions.file_count", "1") + .doesNotContainKey("java.analysis.main.semantic.files_evaluated"); + } + + @Test + void semantic_observation_survives_visitor_failure() throws Exception { + var visitors = mock(VisitorsBridge.class); + doThrow(new IllegalStateException("Visitor failure")).when(visitors).visitFile(any(), anyBoolean()); + var scanner = new JavaAstScanner(null, telemetry, TelemetryKey.JAVA_ANALYSIS_MAIN); + scanner.setVisitorBridge(visitors); + scanSource(scanner, "class A { Missing value; }"); + + assertThat(telemetry.toMap()) + .containsEntry("java.analysis.main.semantic.files_evaluated", "1") + .containsEntry("java.analysis.main.semantic.files_with_resolution_errors", "1") + .containsEntry("java.analysis.main.exceptions.file_count", "1"); + } + + private JavaAstScanner scanner(JavaAnalysisKeys keys) { + var scanner = new JavaAstScanner(null, telemetry, keys); + scanner.setVisitorBridge(mock(VisitorsBridge.class)); + return scanner; + } + + private static void scanSource(JavaAstScanner scanner, String source) throws Exception { + var tree = (JavaTree.CompilationUnitTreeImpl) JParserTestUtils.parse(source); + var result = mock(JParserConfig.Result.class); + when(result.get()).thenReturn(tree); + scanner.simpleScan(TestUtils.emptyInputFile("A.java"), result, ast -> ast.sema.getEnvironmentCleaner().run()); + } +} diff --git a/java-frontend/src/test/java/org/sonar/java/telemetry/TelemetryKeyTest.java b/java-frontend/src/test/java/org/sonar/java/telemetry/TelemetryKeyTest.java index 83c5bf85042..7362bf5df07 100644 --- a/java-frontend/src/test/java/org/sonar/java/telemetry/TelemetryKeyTest.java +++ b/java-frontend/src/test/java/org/sonar/java/telemetry/TelemetryKeyTest.java @@ -42,7 +42,7 @@ void test_TelemetryKey_names() { void testCompoundKeys() { for (String artifact : List.of("main.", "test.", "generated.")) { for (String result : List.of("success.", "parse_errors.", "exceptions.")) { - for (String metric : List.of("size_chars", "time_ms")) { + for (String metric : List.of("size_chars", "time_ms", "file_count")) { assertTelemetryNameExists("java.analysis." + artifact + result + metric); } } @@ -73,7 +73,8 @@ void testAnalysisKeys() { ); Map> metrics = Map.of( "size_chars", SpeedKeys::sizeCharsKey, - "time_ms", SpeedKeys::timeMsKey + "time_ms", SpeedKeys::timeMsKey, + "file_count", SpeedKeys::fileCountKey ); artifacts.forEach((artifactName, artifact) -> { String typeErrorName = artifactName + "success.type_error_count"; diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java index 7ecae776712..2713fbc42ad 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaSensorTest.java @@ -199,6 +199,19 @@ void test_issues_creation_on_main_file() throws IOException { "java.analysis.main.success.size_chars", "java.analysis.main.success.time_ms", "java.analysis.main.success.type_error_count", + "java.analysis.main.success.file_count", + "java.analysis.main.semantic.files_evaluated", + "java.analysis.main.semantic.files_with_resolution_errors", + "java.analysis.main.semantic.resolution_error_count", + "java.analysis.main.semantic.modules_evaluated", + "java.analysis.main.semantic.modules_with_resolution_errors", + "java.classpath.main.module_count", + "java.classpath.main.modules_missing_binaries", + "java.classpath.main.modules_without_binary_entries", + "java.classpath.main.modules_missing_libraries", + "java.classpath.main.modules_without_library_entries", + "java.classpath.main.modules_with_invalid_jdk", + "java.classpath.main.modules_with_unmatched_entries", "java.dependency.lombok", "java.dependency.spring-boot", "java.dependency.spring-web", @@ -208,7 +221,14 @@ void test_issues_creation_on_main_file() throws IOException { "java.scanner_app"); assertThat(telemetryMap.get("java.analysis.main.success.size_chars")).matches("\\d+"); assertThat(telemetryMap.get("java.analysis.main.success.time_ms")).matches("\\d+"); - assertThat(telemetryMap).containsEntry("java.analysis.main.success.type_error_count", EXPECTED_TYPE_ERROR_COUNT); + assertThat(telemetryMap) + .containsEntry("java.analysis.main.success.type_error_count", EXPECTED_TYPE_ERROR_COUNT) + .containsEntry("java.analysis.main.success.file_count", "3") + .containsEntry("java.analysis.main.semantic.files_evaluated", "3") + .containsEntry("java.analysis.main.semantic.files_with_resolution_errors", "3") + .containsEntry("java.analysis.main.semantic.modules_evaluated", "3") + .containsEntry("java.analysis.main.semantic.modules_with_resolution_errors", "3") + .containsEntry("java.classpath.main.module_count", "3"); } @Test @@ -221,6 +241,19 @@ void test_issues_creation_on_test_file() throws IOException { // NOSONAR require "java.analysis.test.success.size_chars", "java.analysis.test.success.time_ms", "java.analysis.test.success.type_error_count", + "java.analysis.test.success.file_count", + "java.analysis.test.semantic.files_evaluated", + "java.analysis.test.semantic.files_with_resolution_errors", + "java.analysis.test.semantic.resolution_error_count", + "java.analysis.test.semantic.modules_evaluated", + "java.analysis.test.semantic.modules_with_resolution_errors", + "java.classpath.test.module_count", + "java.classpath.test.modules_missing_binaries", + "java.classpath.test.modules_without_binary_entries", + "java.classpath.test.modules_missing_libraries", + "java.classpath.test.modules_without_library_entries", + "java.classpath.test.modules_with_invalid_jdk", + "java.classpath.test.modules_with_unmatched_entries", "java.dependency.lombok", "java.dependency.spring-boot", "java.dependency.spring-web", @@ -230,7 +263,14 @@ void test_issues_creation_on_test_file() throws IOException { // NOSONAR require "java.scanner_app"); assertThat(telemetryMap.get("java.analysis.test.success.size_chars")).matches("\\d+"); assertThat(telemetryMap.get("java.analysis.test.success.time_ms")).matches("\\d+"); - assertThat(telemetryMap).containsEntry("java.analysis.test.success.type_error_count", EXPECTED_TYPE_ERROR_COUNT); + assertThat(telemetryMap) + .containsEntry("java.analysis.test.success.type_error_count", EXPECTED_TYPE_ERROR_COUNT) + .containsEntry("java.analysis.test.success.file_count", "3") + .containsEntry("java.analysis.test.semantic.files_evaluated", "3") + .containsEntry("java.analysis.test.semantic.files_with_resolution_errors", "3") + .containsEntry("java.analysis.test.semantic.modules_evaluated", "3") + .containsEntry("java.analysis.test.semantic.modules_with_resolution_errors", "3") + .containsEntry("java.classpath.test.module_count", "3"); } private static int lineNumberOfTheMethodWithNoSonar(FileSystem fs) throws IOException {