Repository: SonarSource/sonarlint-core Branch: master Commit: be6119e3fe92 Files: 1983 Total size: 7.1 MB Directory structure: gitextract_0whgick7/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── renovate.json │ └── workflows/ │ ├── PullRequestClosed.yml │ ├── PullRequestCreated.yml │ ├── RequestReview.yml │ ├── SubmitReview.yml │ ├── build.yml │ ├── full-release.yml │ ├── notify-failure.yml │ ├── releasability.yml │ ├── release.yml │ └── shadow_scans.yml ├── .gitignore ├── .mvn/ │ └── wrapper/ │ └── maven-wrapper.properties ├── .sonarlint/ │ └── connectedMode.json ├── API_CHANGES.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── backend/ │ ├── analysis-engine/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── analysis/ │ │ │ ├── AnalysisQueue.java │ │ │ ├── AnalysisScheduler.java │ │ │ ├── SchedulerResetConfiguration.java │ │ │ ├── api/ │ │ │ │ ├── AnalysisConfiguration.java │ │ │ │ ├── AnalysisResults.java │ │ │ │ ├── AnalysisSchedulerConfiguration.java │ │ │ │ ├── ClientInputFile.java │ │ │ │ ├── ClientInputFileEdit.java │ │ │ │ ├── ClientModuleFileEvent.java │ │ │ │ ├── ClientModuleFileSystem.java │ │ │ │ ├── ClientModuleInfo.java │ │ │ │ ├── DefaultLocation.java │ │ │ │ ├── Flow.java │ │ │ │ ├── Issue.java │ │ │ │ ├── IssueLocation.java │ │ │ │ ├── QuickFix.java │ │ │ │ ├── TextEdit.java │ │ │ │ ├── TriggerType.java │ │ │ │ ├── WithTextRange.java │ │ │ │ └── package-info.java │ │ │ ├── command/ │ │ │ │ ├── AnalyzeCommand.java │ │ │ │ ├── Command.java │ │ │ │ ├── NotifyModuleEventCommand.java │ │ │ │ ├── ResetPluginsCommand.java │ │ │ │ ├── UnregisterModuleCommand.java │ │ │ │ └── package-info.java │ │ │ ├── container/ │ │ │ │ ├── ContainerLifespan.java │ │ │ │ ├── analysis/ │ │ │ │ │ ├── AnalysisConfigurationProvider.java │ │ │ │ │ ├── AnalysisContainer.java │ │ │ │ │ ├── AnalysisSettings.java │ │ │ │ │ ├── AnalysisTempFolderProvider.java │ │ │ │ │ ├── IssueListenerHolder.java │ │ │ │ │ ├── SonarLintPathPattern.java │ │ │ │ │ ├── filesystem/ │ │ │ │ │ │ ├── AbstractFilePredicate.java │ │ │ │ │ │ ├── AndPredicate.java │ │ │ │ │ │ ├── DefaultFilePredicates.java │ │ │ │ │ │ ├── DefaultTextPointer.java │ │ │ │ │ │ ├── DefaultTextRange.java │ │ │ │ │ │ ├── FalsePredicate.java │ │ │ │ │ │ ├── FileExtensionPredicate.java │ │ │ │ │ │ ├── FileIndexer.java │ │ │ │ │ │ ├── FileMetadata.java │ │ │ │ │ │ ├── FilenamePredicate.java │ │ │ │ │ │ ├── InputFileBuilder.java │ │ │ │ │ │ ├── InputFileIndex.java │ │ │ │ │ │ ├── Language.java │ │ │ │ │ │ ├── LanguageDetection.java │ │ │ │ │ │ ├── LanguagePredicate.java │ │ │ │ │ │ ├── NotPredicate.java │ │ │ │ │ │ ├── OptimizedFilePredicate.java │ │ │ │ │ │ ├── OptimizedFilePredicateAdapter.java │ │ │ │ │ │ ├── OrPredicate.java │ │ │ │ │ │ ├── PathPatternPredicate.java │ │ │ │ │ │ ├── ProgressReport.java │ │ │ │ │ │ ├── SonarLintFileSystem.java │ │ │ │ │ │ ├── SonarLintInputDir.java │ │ │ │ │ │ ├── SonarLintInputFile.java │ │ │ │ │ │ ├── SonarLintInputProject.java │ │ │ │ │ │ ├── StatusPredicate.java │ │ │ │ │ │ ├── TruePredicate.java │ │ │ │ │ │ ├── TypePredicate.java │ │ │ │ │ │ ├── URIPredicate.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── issue/ │ │ │ │ │ │ ├── DefaultIssueFilterChain.java │ │ │ │ │ │ ├── IssueFilters.java │ │ │ │ │ │ ├── SensorInputFileEdit.java │ │ │ │ │ │ ├── SensorQuickFix.java │ │ │ │ │ │ ├── SensorTextEdit.java │ │ │ │ │ │ ├── TextRangeUtils.java │ │ │ │ │ │ ├── ignore/ │ │ │ │ │ │ │ ├── EnforceIssuesFilter.java │ │ │ │ │ │ │ ├── IgnoreIssuesFilter.java │ │ │ │ │ │ │ ├── SonarLintNoSonarFilter.java │ │ │ │ │ │ │ ├── package-info.java │ │ │ │ │ │ │ ├── pattern/ │ │ │ │ │ │ │ │ ├── AbstractPatternInitializer.java │ │ │ │ │ │ │ │ ├── BlockIssuePattern.java │ │ │ │ │ │ │ │ ├── IssueExclusionPatternInitializer.java │ │ │ │ │ │ │ │ ├── IssueInclusionPatternInitializer.java │ │ │ │ │ │ │ │ ├── IssuePattern.java │ │ │ │ │ │ │ │ └── package-info.java │ │ │ │ │ │ │ └── scanner/ │ │ │ │ │ │ │ ├── IssueExclusionsLoader.java │ │ │ │ │ │ │ ├── IssueExclusionsRegexpScanner.java │ │ │ │ │ │ │ ├── LineRange.java │ │ │ │ │ │ │ └── package-info.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── sensor/ │ │ │ │ │ ├── SensorOptimizer.java │ │ │ │ │ ├── SensorsExecutor.java │ │ │ │ │ ├── SonarLintSensorStorage.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── global/ │ │ │ │ │ ├── AnalysisExtensionInstaller.java │ │ │ │ │ ├── GlobalAnalysisContainer.java │ │ │ │ │ ├── GlobalConfigurationProvider.java │ │ │ │ │ ├── GlobalExtensionContainer.java │ │ │ │ │ ├── GlobalSettings.java │ │ │ │ │ ├── GlobalTempFolder.java │ │ │ │ │ ├── GlobalTempFolderProvider.java │ │ │ │ │ ├── ModuleRegistry.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── module/ │ │ │ │ │ ├── DefaultModuleFileEvent.java │ │ │ │ │ ├── ModuleContainer.java │ │ │ │ │ ├── ModuleFileEventNotifier.java │ │ │ │ │ ├── ModuleInputFileBuilder.java │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── sonarapi/ │ │ │ ├── ActiveRulesAdapter.java │ │ │ ├── DefaultAnalysisError.java │ │ │ ├── DefaultFilterableIssue.java │ │ │ ├── DefaultFlow.java │ │ │ ├── DefaultSensorContext.java │ │ │ ├── DefaultSensorDescriptor.java │ │ │ ├── DefaultSonarLintIssue.java │ │ │ ├── DefaultSonarLintIssueLocation.java │ │ │ ├── DefaultStorable.java │ │ │ ├── DefaultTempFolder.java │ │ │ ├── SonarLintModuleFileSystem.java │ │ │ ├── noop/ │ │ │ │ ├── NoOpFileLinesContext.java │ │ │ │ ├── NoOpFileLinesContextFactory.java │ │ │ │ ├── NoOpNewCoverage.java │ │ │ │ ├── NoOpNewCpdTokens.java │ │ │ │ ├── NoOpNewHighlighting.java │ │ │ │ ├── NoOpNewMeasure.java │ │ │ │ ├── NoOpNewMessageFormatting.java │ │ │ │ ├── NoOpNewSignificantCode.java │ │ │ │ ├── NoOpNewSymbolTable.java │ │ │ │ └── package-info.java │ │ │ └── package-info.java │ │ └── test/ │ │ ├── java/ │ │ │ ├── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── analysis/ │ │ │ │ ├── AnalysisQueueTest.java │ │ │ │ ├── api/ │ │ │ │ │ ├── AnalysisConfigurationTests.java │ │ │ │ │ ├── AnalysisSchedulerConfigurationTests.java │ │ │ │ │ ├── ClientInputFileTests.java │ │ │ │ │ ├── DefaultLocationTests.java │ │ │ │ │ └── IssueLocationTests.java │ │ │ │ ├── command/ │ │ │ │ │ └── AnalyzeCommandTest.java │ │ │ │ ├── container/ │ │ │ │ │ ├── analysis/ │ │ │ │ │ │ ├── AnalysisSettingsTest.java │ │ │ │ │ │ ├── AnalysisTempFolderProviderTests.java │ │ │ │ │ │ ├── SonarLintPathPatternTests.java │ │ │ │ │ │ ├── filesystem/ │ │ │ │ │ │ │ ├── DefaultFilePredicatesTests.java │ │ │ │ │ │ │ ├── InputFileBuilderTests.java │ │ │ │ │ │ │ ├── InputFileCacheTests.java │ │ │ │ │ │ │ ├── LanguageDetectionTests.java │ │ │ │ │ │ │ ├── ProgressReportTests.java │ │ │ │ │ │ │ ├── SonarLintFileSystemTests.java │ │ │ │ │ │ │ ├── SonarLintInputDirTests.java │ │ │ │ │ │ │ └── SonarLintInputFileTests.java │ │ │ │ │ │ ├── issue/ │ │ │ │ │ │ │ └── ignore/ │ │ │ │ │ │ │ ├── EnforceIssuesFilterTests.java │ │ │ │ │ │ │ ├── IgnoreIssuesFilterTests.java │ │ │ │ │ │ │ ├── pattern/ │ │ │ │ │ │ │ │ ├── IssueExclusionPatternInitializerTests.java │ │ │ │ │ │ │ │ ├── IssueInclusionPatternInitializerTests.java │ │ │ │ │ │ │ │ └── IssuePatternTests.java │ │ │ │ │ │ │ └── scanner/ │ │ │ │ │ │ │ ├── IssueExclusionsLoaderTests.java │ │ │ │ │ │ │ ├── IssueExclusionsRegexpScannerTests.java │ │ │ │ │ │ │ └── LineRangeTests.java │ │ │ │ │ │ └── sensor/ │ │ │ │ │ │ ├── SensorOptimizerTests.java │ │ │ │ │ │ ├── SensorsExecutorTests.java │ │ │ │ │ │ └── SonarLintSensorStorageTests.java │ │ │ │ │ ├── global/ │ │ │ │ │ │ ├── AnalysisExtensionInstallerTests.java │ │ │ │ │ │ ├── GlobalSettingsTests.java │ │ │ │ │ │ └── GlobalTempFolderProviderTests.java │ │ │ │ │ └── module/ │ │ │ │ │ └── ModuleInputFileBuilderTests.java │ │ │ │ ├── mediumtests/ │ │ │ │ │ └── AnalysisSchedulerMediumTests.java │ │ │ │ └── sonarapi/ │ │ │ │ ├── DefaultAnalysisErrorTests.java │ │ │ │ ├── DefaultFilterableIssueTests.java │ │ │ │ ├── DefaultSensorContextTests.java │ │ │ │ ├── DefaultSensorDescriptorTests.java │ │ │ │ ├── DefaultSonarLintIssueTests.java │ │ │ │ ├── DefaultTempFolderTests.java │ │ │ │ └── noop/ │ │ │ │ ├── NoOpNewCoverageTests.java │ │ │ │ ├── NoOpNewCpdTokensTests.java │ │ │ │ ├── NoOpNewHighlightingTests.java │ │ │ │ ├── NoOpNewMeasureTests.java │ │ │ │ ├── NoOpNewMessageFormattingTest.java │ │ │ │ ├── NoOpNewSignificantCodeTests.java │ │ │ │ └── NoOpNewSymbolTableTests.java │ │ │ └── testutils/ │ │ │ ├── FileUtils.java │ │ │ ├── InMemoryTestClientInputFile.java │ │ │ ├── OnDiskTestClientInputFile.java │ │ │ ├── TestClientInputFile.java │ │ │ └── TestInputFileBuilder.java │ │ └── resources/ │ │ ├── IssueExclusionsRegexpScannerTests/ │ │ │ ├── file-with-double-regexp-mess.txt │ │ │ ├── file-with-double-regexp-twice.txt │ │ │ ├── file-with-double-regexp-unfinished.txt │ │ │ ├── file-with-double-regexp-wrong-order.txt │ │ │ ├── file-with-double-regexp.txt │ │ │ ├── file-with-no-regexp.txt │ │ │ ├── file-with-single-regexp-and-double-regexp.txt │ │ │ ├── file-with-single-regexp-last-line.txt │ │ │ └── file-with-single-regexp.txt │ │ └── logback-test.xml │ ├── cli/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── assembly/ │ │ │ │ ├── dist-linux_aarch64.xml │ │ │ │ ├── dist-linux_x64.xml │ │ │ │ ├── dist-macosx_aarch64.xml │ │ │ │ ├── dist-macosx_x64.xml │ │ │ │ ├── dist-no-arch.xml │ │ │ │ └── dist-windows_x64.xml │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── backend/ │ │ │ └── cli/ │ │ │ ├── EndOfStreamAwareInputStream.java │ │ │ ├── SonarLintServerCli.java │ │ │ └── package-info.java │ │ └── test/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── backend/ │ │ └── cli/ │ │ ├── EndOfStreamAwareInputStreamTest.java │ │ └── SonarLintServerCliTest.java │ ├── commons/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── commons/ │ │ │ │ ├── CleanCodeAttribute.java │ │ │ │ ├── CleanCodeAttributeCategory.java │ │ │ │ ├── ConnectionKind.java │ │ │ │ ├── HotspotReviewStatus.java │ │ │ │ ├── IOExceptionUtils.java │ │ │ │ ├── ImpactSeverity.java │ │ │ │ ├── IssueSeverity.java │ │ │ │ ├── IssueStatus.java │ │ │ │ ├── KnownFinding.java │ │ │ │ ├── KnownFindingType.java │ │ │ │ ├── LineWithHash.java │ │ │ │ ├── LocalOnlyIssue.java │ │ │ │ ├── LocalOnlyIssueResolution.java │ │ │ │ ├── MultiFileBlameResult.java │ │ │ │ ├── NewCodeDefinition.java │ │ │ │ ├── NewCodeMode.java │ │ │ │ ├── RuleKey.java │ │ │ │ ├── RuleType.java │ │ │ │ ├── SoftwareQuality.java │ │ │ │ ├── SonarLintCoreVersion.java │ │ │ │ ├── SonarLintException.java │ │ │ │ ├── SonarLintGitIgnore.java │ │ │ │ ├── SonarLintUserHome.java │ │ │ │ ├── Transition.java │ │ │ │ ├── Version.java │ │ │ │ ├── VulnerabilityProbability.java │ │ │ │ ├── api/ │ │ │ │ │ ├── SonarLanguage.java │ │ │ │ │ ├── TextRange.java │ │ │ │ │ ├── TextRangeWithHash.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── progress/ │ │ │ │ │ ├── CanceledException.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── dogfood/ │ │ │ │ │ ├── DogfoodEnvironmentDetectionService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── log/ │ │ │ │ │ ├── FormattingTuple.java │ │ │ │ │ ├── LogOutput.java │ │ │ │ │ ├── MessageFormatter.java │ │ │ │ │ ├── NormalizedParameters.java │ │ │ │ │ ├── SonarLintLogger.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── plugins/ │ │ │ │ │ ├── Dependency.java │ │ │ │ │ ├── EnterpriseReplacement.java │ │ │ │ │ ├── SonarArtifact.java │ │ │ │ │ ├── SonarPlugin.java │ │ │ │ │ ├── SonarPluginDependency.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── progress/ │ │ │ │ │ ├── ExecutorServiceShutdownWatchable.java │ │ │ │ │ ├── NoOpProgressMonitor.java │ │ │ │ │ ├── ProgressIndicator.java │ │ │ │ │ ├── ProgressMonitor.java │ │ │ │ │ ├── SonarLintCancelMonitor.java │ │ │ │ │ ├── Task.java │ │ │ │ │ ├── TaskManager.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── storage/ │ │ │ │ │ ├── DatabaseExceptionReporter.java │ │ │ │ │ ├── JooqDatabaseExceptionListener.java │ │ │ │ │ ├── SonarLintDatabase.java │ │ │ │ │ ├── XodusPurgeUtils.java │ │ │ │ │ ├── adapter/ │ │ │ │ │ │ ├── LocalDateAdapter.java │ │ │ │ │ │ ├── LocalDateTimeAdapter.java │ │ │ │ │ │ ├── OffsetDateTimeAdapter.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── local/ │ │ │ │ │ │ ├── FileStorageManager.java │ │ │ │ │ │ ├── LocalStorage.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── tracing/ │ │ │ │ │ ├── Span.java │ │ │ │ │ ├── Step.java │ │ │ │ │ ├── Trace.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── util/ │ │ │ │ │ ├── FailSafeExecutors.java │ │ │ │ │ ├── FileUtils.java │ │ │ │ │ ├── StringUtils.java │ │ │ │ │ ├── git/ │ │ │ │ │ │ ├── BlameResult.java │ │ │ │ │ │ ├── GitBlameReader.java │ │ │ │ │ │ ├── GitService.java │ │ │ │ │ │ ├── NativeGit.java │ │ │ │ │ │ ├── NativeGitLocator.java │ │ │ │ │ │ ├── ProcessWrapperFactory.java │ │ │ │ │ │ ├── exceptions/ │ │ │ │ │ │ │ ├── GitException.java │ │ │ │ │ │ │ ├── GitRepoNotFoundException.java │ │ │ │ │ │ │ └── package-info.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ └── validation/ │ │ │ │ ├── InvalidFields.java │ │ │ │ ├── RegexpValidator.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ ├── db/ │ │ │ │ └── migration/ │ │ │ │ ├── README_BEFORE_TOUCHING_THIS_FOLDER.md │ │ │ │ ├── V1__init_database.sql │ │ │ │ ├── V2__create_local_only_issues_table.sql │ │ │ │ ├── V3__allow_longer_messages_for_known_findings_table.sql │ │ │ │ ├── V4__allow_longer_file_paths.sql │ │ │ │ └── V5__allow_longer_configuration_scope_ids.sql │ │ │ ├── logback-shared.xml │ │ │ └── sl_core_version.txt │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── commons/ │ │ │ ├── HotspotReviewStatusTest.java │ │ │ ├── IOExceptionUtilsTests.java │ │ │ ├── LogTestStartAndEnd.java │ │ │ ├── MultiFileBlameResultTest.java │ │ │ ├── NewCodeDefinitionTests.java │ │ │ ├── RuleKeyTests.java │ │ │ ├── SonarLintCoreVersionTests.java │ │ │ ├── SonarLintUserHomeTests.java │ │ │ ├── StringUtilsTests.java │ │ │ ├── TextRangeTests.java │ │ │ ├── TextRangeWithHashTests.java │ │ │ ├── VersionTests.java │ │ │ ├── log/ │ │ │ │ ├── ConcurrentListAppender.java │ │ │ │ ├── MessageFormatterTests.java │ │ │ │ ├── SonarLintLogTester.java │ │ │ │ └── SonarLintLoggerTests.java │ │ │ ├── progress/ │ │ │ │ └── ExecutorServiceShutdownWatchableTests.java │ │ │ ├── storage/ │ │ │ │ ├── DatabaseExceptionReporterTests.java │ │ │ │ ├── JooqDatabaseExceptionListenerTests.java │ │ │ │ ├── SonarLintDatabaseExceptionTests.java │ │ │ │ └── local/ │ │ │ │ └── FileStorageManagerTest.java │ │ │ ├── testutils/ │ │ │ │ ├── GitUtils.java │ │ │ │ └── MockWebServerExtension.java │ │ │ ├── util/ │ │ │ │ └── git/ │ │ │ │ ├── BlameParserTests.java │ │ │ │ ├── GitServiceTests.java │ │ │ │ ├── NativeGitLocatorTests.java │ │ │ │ ├── NativeGitTest.java │ │ │ │ └── ProcessWrapperFactoryTests.java │ │ │ └── validation/ │ │ │ ├── InvalidFieldsTest.java │ │ │ └── RegexpValidatorTest.java │ │ └── resources/ │ │ └── logback.xml │ ├── core/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ ├── BindingCandidatesFinder.java │ │ │ │ ├── BindingClueProvider.java │ │ │ │ ├── BindingSuggestionProvider.java │ │ │ │ ├── ConfigurationScopeSharedContext.java │ │ │ │ ├── ConfigurationService.java │ │ │ │ ├── ConnectionService.java │ │ │ │ ├── ConnectionSuggestionProvider.java │ │ │ │ ├── DtoMapper.java │ │ │ │ ├── MCPServerConfigurationProvider.java │ │ │ │ ├── OrganizationsCache.java │ │ │ │ ├── ServerFileExclusions.java │ │ │ │ ├── SharedConnectedModeSettingsProvider.java │ │ │ │ ├── SonarCloudActiveEnvironment.java │ │ │ │ ├── SonarCloudRegion.java │ │ │ │ ├── SonarCodeContextService.java │ │ │ │ ├── SonarLintMDC.java │ │ │ │ ├── SonarProjectsCache.java │ │ │ │ ├── SonarQubeClientManager.java │ │ │ │ ├── TextSearchIndex.java │ │ │ │ ├── TokenGeneratorHelper.java │ │ │ │ ├── UserPaths.java │ │ │ │ ├── VersionSoonUnsupportedHelper.java │ │ │ │ ├── active/ │ │ │ │ │ └── rules/ │ │ │ │ │ ├── ActiveRuleDetails.java │ │ │ │ │ ├── ActiveRulesService.java │ │ │ │ │ ├── ServerActiveRulesChanged.java │ │ │ │ │ ├── StandaloneRulesConfigurationChanged.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── ai/ │ │ │ │ │ └── ide/ │ │ │ │ │ ├── AiAgentService.java │ │ │ │ │ ├── AiHookService.java │ │ │ │ │ ├── ExecutableLocator.java │ │ │ │ │ ├── HookScriptType.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── analysis/ │ │ │ │ │ ├── AnalysisFailedEvent.java │ │ │ │ │ ├── AnalysisFinishedEvent.java │ │ │ │ │ ├── AnalysisResult.java │ │ │ │ │ ├── AnalysisSchedulerCache.java │ │ │ │ │ ├── AnalysisService.java │ │ │ │ │ ├── AnalysisStartedEvent.java │ │ │ │ │ ├── AutomaticAnalysisSettingChangedEvent.java │ │ │ │ │ ├── BackendInputFile.java │ │ │ │ │ ├── BackendModuleFileSystem.java │ │ │ │ │ ├── ClientNodeJsPathChanged.java │ │ │ │ │ ├── IssuesRaisedEvent.java │ │ │ │ │ ├── NodeJsService.java │ │ │ │ │ ├── RawIssue.java │ │ │ │ │ ├── RawIssueDetectedEvent.java │ │ │ │ │ ├── UserAnalysisPropertiesRepository.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── branch/ │ │ │ │ │ ├── MatchedSonarProjectBranchChangedEvent.java │ │ │ │ │ ├── SonarProjectBranchMatchingEndedEvent.java │ │ │ │ │ ├── SonarProjectBranchTrackingService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── commons/ │ │ │ │ │ ├── Binding.java │ │ │ │ │ ├── BoundScope.java │ │ │ │ │ ├── DebounceComputer.java │ │ │ │ │ ├── SmartCancelableLoadingCache.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── connection/ │ │ │ │ │ ├── SonarQubeClient.java │ │ │ │ │ ├── SonarQubeClientState.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── embedded/ │ │ │ │ │ └── server/ │ │ │ │ │ ├── AnalyzeFileListRequestHandler.java │ │ │ │ │ ├── AttributeUtils.java │ │ │ │ │ ├── AwaitingUserTokenFutureRepository.java │ │ │ │ │ ├── EmbeddedServer.java │ │ │ │ │ ├── RequestHandlerBindingAssistant.java │ │ │ │ │ ├── ToggleAutomaticAnalysisRequestHandler.java │ │ │ │ │ ├── filter/ │ │ │ │ │ │ ├── CorsFilter.java │ │ │ │ │ │ ├── CspFilter.java │ │ │ │ │ │ ├── ParseParamsFilter.java │ │ │ │ │ │ ├── RateLimitFilter.java │ │ │ │ │ │ ├── ValidationFilter.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── handler/ │ │ │ │ │ │ ├── GeneratedUserTokenHandler.java │ │ │ │ │ │ ├── ShowFixSuggestionRequestHandler.java │ │ │ │ │ │ ├── ShowHotspotRequestHandler.java │ │ │ │ │ │ ├── ShowIssueRequestHandler.java │ │ │ │ │ │ ├── StatusRequestHandler.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── event/ │ │ │ │ │ ├── BindingConfigChangedEvent.java │ │ │ │ │ ├── ConfigurationScopeRemovedEvent.java │ │ │ │ │ ├── ConfigurationScopesAddedWithBindingEvent.java │ │ │ │ │ ├── ConnectionConfigurationAddedEvent.java │ │ │ │ │ ├── ConnectionConfigurationRemovedEvent.java │ │ │ │ │ ├── ConnectionConfigurationUpdatedEvent.java │ │ │ │ │ ├── ConnectionCredentialsChangedEvent.java │ │ │ │ │ ├── DependencyRisksSynchronizedEvent.java │ │ │ │ │ ├── FixSuggestionReceivedEvent.java │ │ │ │ │ ├── LocalOnlyIssueStatusChangedEvent.java │ │ │ │ │ ├── MatchingSessionEndedEvent.java │ │ │ │ │ ├── PluginStatusUpdateEvent.java │ │ │ │ │ ├── ServerIssueStatusChangedEvent.java │ │ │ │ │ ├── SonarServerEventReceivedEvent.java │ │ │ │ │ ├── TaintVulnerabilitiesSynchronizedEvent.java │ │ │ │ │ ├── TelemetryUpdatedEvent.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FilePathTranslation.java │ │ │ │ │ ├── PathTranslationService.java │ │ │ │ │ ├── ServerFilePathsProvider.java │ │ │ │ │ ├── WindowsShortcutUtils.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── fs/ │ │ │ │ │ ├── ClientFile.java │ │ │ │ │ ├── ClientFileSystemService.java │ │ │ │ │ ├── FileExclusionService.java │ │ │ │ │ ├── FileOpenedEvent.java │ │ │ │ │ ├── FileSystemUpdatedEvent.java │ │ │ │ │ ├── OpenFilesRepository.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── hotspot/ │ │ │ │ │ ├── HotspotService.java │ │ │ │ │ ├── HotspotStatusChangeException.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── http/ │ │ │ │ │ ├── AskClientCertificatePredicate.java │ │ │ │ │ ├── ClientProxyCredentialsProvider.java │ │ │ │ │ ├── ClientProxySelector.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── issue/ │ │ │ │ │ ├── IssueNotFoundException.java │ │ │ │ │ ├── IssueService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── labs/ │ │ │ │ │ ├── IdeLabsHttpClient.java │ │ │ │ │ ├── IdeLabsService.java │ │ │ │ │ ├── IdeLabsSpringConfig.java │ │ │ │ │ ├── IdeLabsSubscriptionRequestPayload.java │ │ │ │ │ ├── IdeLabsSubscriptionResponseBody.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── languages/ │ │ │ │ │ ├── LanguageSupportRepository.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── local/ │ │ │ │ │ └── only/ │ │ │ │ │ ├── IssueStatusBinding.java │ │ │ │ │ ├── XodusLocalOnlyIssueStorageService.java │ │ │ │ │ ├── XodusLocalOnlyIssueStore.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── log/ │ │ │ │ │ ├── LogService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── mode/ │ │ │ │ │ ├── SeverityModeService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── monitoring/ │ │ │ │ │ ├── MonitoringInitializationParams.java │ │ │ │ │ ├── MonitoringService.java │ │ │ │ │ ├── MonitoringUserIdStore.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── newcode/ │ │ │ │ │ ├── NewCodeService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── nodejs/ │ │ │ │ │ ├── InstalledNodeJs.java │ │ │ │ │ ├── NodeJsHelper.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── plugin/ │ │ │ │ │ ├── ArtifactSource.java │ │ │ │ │ ├── DotnetSupport.java │ │ │ │ │ ├── PluginJarUtils.java │ │ │ │ │ ├── PluginLifecycleService.java │ │ │ │ │ ├── PluginStatus.java │ │ │ │ │ ├── PluginStatusMapper.java │ │ │ │ │ ├── PluginStatusNotifierService.java │ │ │ │ │ ├── PluginStatusesChangedEvent.java │ │ │ │ │ ├── PluginsConfiguration.java │ │ │ │ │ ├── PluginsRepository.java │ │ │ │ │ ├── PluginsService.java │ │ │ │ │ ├── loading/ │ │ │ │ │ │ └── strategy/ │ │ │ │ │ │ ├── ArtifactCandidate.java │ │ │ │ │ │ ├── ArtifactsLoadingResult.java │ │ │ │ │ │ ├── ArtifactsLoadingStrategy.java │ │ │ │ │ │ ├── BaseArtifactsLoadingStrategy.java │ │ │ │ │ │ ├── ConnectedArtifactsLoadingStrategy.java │ │ │ │ │ │ ├── ConnectedArtifactsLoadingStrategyFactory.java │ │ │ │ │ │ ├── StandaloneArtifactsLoadingStrategy.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── skipped/ │ │ │ │ │ │ ├── SkippedPlugin.java │ │ │ │ │ │ ├── SkippedPluginsNotifierService.java │ │ │ │ │ │ ├── SkippedPluginsRepository.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── source/ │ │ │ │ │ ├── ArtifactKind.java │ │ │ │ │ ├── ArtifactOrigin.java │ │ │ │ │ ├── ArtifactSource.java │ │ │ │ │ ├── ArtifactState.java │ │ │ │ │ ├── AvailableArtifact.java │ │ │ │ │ ├── LoadResult.java │ │ │ │ │ ├── ResolvedArtifact.java │ │ │ │ │ ├── UniqueTaskExecutor.java │ │ │ │ │ ├── binaries/ │ │ │ │ │ │ ├── BinariesArtifact.java │ │ │ │ │ │ ├── BinariesArtifactSource.java │ │ │ │ │ │ ├── BinariesLocalCacheManager.java │ │ │ │ │ │ ├── BinariesSignatureVerifier.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── embedded/ │ │ │ │ │ │ ├── EmbeddedPluginSource.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── server/ │ │ │ │ │ ├── ServerPluginDownloader.java │ │ │ │ │ ├── ServerPluginSource.java │ │ │ │ │ ├── ServerPluginsCache.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── progress/ │ │ │ │ │ ├── ClientAwareProgressMonitor.java │ │ │ │ │ ├── ClientAwareTaskManager.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── promotion/ │ │ │ │ │ ├── LanguagePromotionService.java │ │ │ │ │ ├── PromotionSpringConfig.java │ │ │ │ │ ├── campaign/ │ │ │ │ │ │ ├── CampaignConstants.java │ │ │ │ │ │ ├── CampaignResolvedEvent.java │ │ │ │ │ │ ├── CampaignService.java │ │ │ │ │ │ ├── CampaignShownEvent.java │ │ │ │ │ │ ├── FeedbackNotificationActionItem.java │ │ │ │ │ │ ├── package-info.java │ │ │ │ │ │ └── storage/ │ │ │ │ │ │ ├── CampaignsLocalStorage.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── remediation/ │ │ │ │ │ └── aicodefix/ │ │ │ │ │ ├── AiCodeFixFeature.java │ │ │ │ │ ├── AiCodeFixService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── reporting/ │ │ │ │ │ ├── FindingReportingService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── repository/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── BindingConfiguration.java │ │ │ │ │ │ ├── ConfigurationRepository.java │ │ │ │ │ │ ├── ConfigurationScope.java │ │ │ │ │ │ ├── ConfigurationScopeWithBinding.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── connection/ │ │ │ │ │ │ ├── AbstractConnectionConfiguration.java │ │ │ │ │ │ ├── ConnectionConfigurationRepository.java │ │ │ │ │ │ ├── SonarCloudConnectionConfiguration.java │ │ │ │ │ │ ├── SonarQubeConnectionConfiguration.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── reporting/ │ │ │ │ │ │ ├── PreviouslyRaisedFindingsRepository.java │ │ │ │ │ │ ├── RaisedIssue.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── rules/ │ │ │ │ │ ├── RulesRepository.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── rules/ │ │ │ │ │ ├── CleanCodePrinciples.java │ │ │ │ │ ├── OthersSectionHtmlContent.java │ │ │ │ │ ├── RuleDetails.java │ │ │ │ │ ├── RuleDetailsAdapter.java │ │ │ │ │ ├── RuleNotFoundException.java │ │ │ │ │ ├── RulesExtractionHelper.java │ │ │ │ │ ├── RulesService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── sca/ │ │ │ │ │ ├── DependencyRiskService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── server/ │ │ │ │ │ └── event/ │ │ │ │ │ ├── ServerEventsService.java │ │ │ │ │ ├── SonarQubeEventStream.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── smartnotifications/ │ │ │ │ │ ├── LastEventPolling.java │ │ │ │ │ ├── SmartNotifications.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── spring/ │ │ │ │ │ ├── SonarLintSpringAppConfig.java │ │ │ │ │ ├── SpringApplicationContextInitializer.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── storage/ │ │ │ │ │ ├── SonarLintDatabaseService.java │ │ │ │ │ ├── StorageService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── sync/ │ │ │ │ │ ├── AnalyzerConfigurationSynchronized.java │ │ │ │ │ ├── BranchBinding.java │ │ │ │ │ ├── ConfigurationScopesSynchronizedEvent.java │ │ │ │ │ ├── FindingsSynchronizationService.java │ │ │ │ │ ├── HotspotSynchronizationService.java │ │ │ │ │ ├── IssueSynchronizationService.java │ │ │ │ │ ├── PluginsSynchronizedEvent.java │ │ │ │ │ ├── ScaSynchronizationService.java │ │ │ │ │ ├── SonarProjectBranchesChangedEvent.java │ │ │ │ │ ├── SonarProjectBranchesSynchronizationService.java │ │ │ │ │ ├── SynchronizationService.java │ │ │ │ │ ├── SynchronizationTimestampRepository.java │ │ │ │ │ ├── TaintSynchronizationService.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── telemetry/ │ │ │ │ │ ├── TelemetryServerAttributesProvider.java │ │ │ │ │ ├── TelemetryService.java │ │ │ │ │ ├── TelemetrySpringConfig.java │ │ │ │ │ ├── gessie/ │ │ │ │ │ │ ├── GessieSpringConfig.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── tracking/ │ │ │ │ │ ├── IntroductionDateProvider.java │ │ │ │ │ ├── IssueMapper.java │ │ │ │ │ ├── IssueStatusBinding.java │ │ │ │ │ ├── KnownFindings.java │ │ │ │ │ ├── LocalOnlyIssueRepository.java │ │ │ │ │ ├── LocalOnlySecurityHotspot.java │ │ │ │ │ ├── TaintVulnerabilityTrackingService.java │ │ │ │ │ ├── TextRangeUtils.java │ │ │ │ │ ├── TrackedIssue.java │ │ │ │ │ ├── TrackingService.java │ │ │ │ │ ├── XodusKnownFindingsStorageService.java │ │ │ │ │ ├── XodusKnownFindingsStore.java │ │ │ │ │ ├── matching/ │ │ │ │ │ │ ├── IssueMatcher.java │ │ │ │ │ │ ├── KnownIssueMatchingAttributesMapper.java │ │ │ │ │ │ ├── LocalOnlyIssueMatchingAttributesMapper.java │ │ │ │ │ │ ├── MatchingAttributesMapper.java │ │ │ │ │ │ ├── MatchingResult.java │ │ │ │ │ │ ├── MatchingSession.java │ │ │ │ │ │ ├── RawIssueFindingMatchingAttributeMapper.java │ │ │ │ │ │ ├── ServerHotspotMatchingAttributesMapper.java │ │ │ │ │ │ ├── ServerIssueMatchingAttributesMapper.java │ │ │ │ │ │ ├── TrackedIssueFindingMatchingAttributeMapper.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── streaming/ │ │ │ │ │ ├── Alarm.java │ │ │ │ │ └── package-info.java │ │ │ │ └── websocket/ │ │ │ │ ├── History.java │ │ │ │ ├── SonarCloudWebSocket.java │ │ │ │ ├── WebSocketEventSubscribePayload.java │ │ │ │ ├── WebSocketManager.java │ │ │ │ ├── WebSocketService.java │ │ │ │ ├── events/ │ │ │ │ │ ├── SmartNotificationEvent.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── parsing/ │ │ │ │ ├── SmartNotificationEventParser.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ ├── ai/ │ │ │ │ └── hooks/ │ │ │ │ ├── sonarqube_analysis_hook.js │ │ │ │ ├── sonarqube_analysis_hook.py │ │ │ │ └── sonarqube_analysis_hook.sh │ │ │ ├── clean-code-principles/ │ │ │ │ ├── defense_in_depth.html │ │ │ │ └── never_trust_user_input.html │ │ │ ├── context-rule-description/ │ │ │ │ └── others_section_html_content.html │ │ │ └── ondemand/ │ │ │ ├── plugins.properties │ │ │ └── sonarsource-public.key │ │ └── test/ │ │ ├── java/ │ │ │ ├── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ ├── BindingCandidatesFinderTests.java │ │ │ │ ├── BindingClueProviderTests.java │ │ │ │ ├── BindingSuggestionProviderTests.java │ │ │ │ ├── ConfigurationServiceTests.java │ │ │ │ ├── ConnectionServiceTests.java │ │ │ │ ├── DtoMapperTests.java │ │ │ │ ├── SonarCloudActiveEnvironmentTests.java │ │ │ │ ├── SonarProjectsCacheTests.java │ │ │ │ ├── SonarQubeClientManagerTests.java │ │ │ │ ├── TelemetryServerAttributesProviderTests.java │ │ │ │ ├── UserPathsTests.java │ │ │ │ ├── VersionSoonUnsupportedHelperTests.java │ │ │ │ ├── ai/ │ │ │ │ │ └── ide/ │ │ │ │ │ ├── AiHookServiceTests.java │ │ │ │ │ └── ExecutableLocatorTests.java │ │ │ │ ├── analysis/ │ │ │ │ │ ├── AnalysisSchedulerCacheTest.java │ │ │ │ │ ├── BackendInputFileTests.java │ │ │ │ │ └── ClientAnalysisPropertiesServiceTests.java │ │ │ │ ├── branch/ │ │ │ │ │ └── SonarProjectBranchTrackingServiceTests.java │ │ │ │ ├── commons/ │ │ │ │ │ └── SmartCancelableLoadingCacheTests.java │ │ │ │ ├── embedded/ │ │ │ │ │ └── server/ │ │ │ │ │ ├── AnalyzeFileListRequestHandlerTests.java │ │ │ │ │ ├── ToggleAutomaticAnalysisRequestHandlerTests.java │ │ │ │ │ ├── filter/ │ │ │ │ │ │ ├── CspFilterTest.java │ │ │ │ │ │ └── RateLimitFilterTests.java │ │ │ │ │ └── handler/ │ │ │ │ │ ├── ShowFixSuggestionRequestHandlerTests.java │ │ │ │ │ └── ShowIssueRequestHandlerTests.java │ │ │ │ ├── file/ │ │ │ │ │ ├── FilePathTranslationTests.java │ │ │ │ │ ├── PathTranslationServiceTests.java │ │ │ │ │ └── ServerFilePathsProviderTest.java │ │ │ │ ├── fs/ │ │ │ │ │ ├── ClientFileTests.java │ │ │ │ │ └── FileExclusionServiceTests.java │ │ │ │ ├── hotspot/ │ │ │ │ │ └── HotspotServiceTests.java │ │ │ │ ├── issue/ │ │ │ │ │ └── matching/ │ │ │ │ │ └── IssueMatcherTests.java │ │ │ │ ├── monitoring/ │ │ │ │ │ └── MonitoringUserIdStoreTests.java │ │ │ │ ├── newcode/ │ │ │ │ │ └── NewCodeServiceTests.java │ │ │ │ ├── nodejs/ │ │ │ │ │ └── NodeJsHelperTests.java │ │ │ │ ├── plugin/ │ │ │ │ │ ├── DotnetSupportTest.java │ │ │ │ │ ├── PluginJarUtilsTest.java │ │ │ │ │ ├── PluginLifecycleServiceTest.java │ │ │ │ │ ├── PluginStatusNotifierServiceTest.java │ │ │ │ │ ├── PluginsServiceTest.java │ │ │ │ │ ├── loading/ │ │ │ │ │ │ └── strategy/ │ │ │ │ │ │ ├── ConnectedArtifactsLoadingStrategyTest.java │ │ │ │ │ │ └── StandaloneArtifactsLoadingStrategyTest.java │ │ │ │ │ └── source/ │ │ │ │ │ ├── BinariesArtifactTest.java │ │ │ │ │ ├── binaries/ │ │ │ │ │ │ ├── BinariesArtifactSourceTest.java │ │ │ │ │ │ ├── BinariesLocalCacheManagerTest.java │ │ │ │ │ │ └── BinariesSignatureVerifierTest.java │ │ │ │ │ ├── embedded/ │ │ │ │ │ │ └── EmbeddedPluginSourceTest.java │ │ │ │ │ └── server/ │ │ │ │ │ ├── ServerPluginDownloaderTest.java │ │ │ │ │ ├── ServerPluginSourceTest.java │ │ │ │ │ └── ServerPluginsCacheTest.java │ │ │ │ ├── progress/ │ │ │ │ │ └── ClientAwareTaskManagerTest.java │ │ │ │ ├── remediation/ │ │ │ │ │ └── aicodefix/ │ │ │ │ │ └── AiCodeFixServiceTest.java │ │ │ │ ├── repository/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── BindingConfigurationTest.java │ │ │ │ │ │ └── ConfigurationRepositoryTest.java │ │ │ │ │ ├── connection/ │ │ │ │ │ │ ├── SonarCloudConnectionConfigurationTest.java │ │ │ │ │ │ └── SonarQubeConnectionConfigurationTest.java │ │ │ │ │ └── rules/ │ │ │ │ │ └── RulesRepositoryTest.java │ │ │ │ ├── rules/ │ │ │ │ │ ├── RuleDetailsAdapterTests.java │ │ │ │ │ ├── RulesFixtures.java │ │ │ │ │ └── RulesServiceTests.java │ │ │ │ ├── sca/ │ │ │ │ │ └── DependencyRiskServiceTests.java │ │ │ │ ├── smartnotifications/ │ │ │ │ │ └── LastEventPollingTests.java │ │ │ │ ├── sync/ │ │ │ │ │ └── BranchBindingTest.java │ │ │ │ ├── tracking/ │ │ │ │ │ ├── KnownFindingMatchingAttributesMapperTests.java │ │ │ │ │ ├── LocalOnlyIssueMatchingAttributesMapperTests.java │ │ │ │ │ ├── ServerHotspotMatchingAttributesMapperTests.java │ │ │ │ │ └── ServerIssueMatchingAttributesMapperTests.java │ │ │ │ └── websocket/ │ │ │ │ ├── SonarCloudWebSocketTests.java │ │ │ │ └── parsing/ │ │ │ │ └── SmartNotificationEventParserTests.java │ │ │ └── testutils/ │ │ │ ├── LocalOnlyIssueFixtures.java │ │ │ ├── TakeThreadDumpAfter.java │ │ │ ├── TestUtils.java │ │ │ └── ThreadDumpExtension.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── ondemand/ │ │ ├── sonar-cpp-compressed-plugin.jar.asc │ │ ├── sonar-cpp-corrupt-plugin.jar.asc │ │ ├── sonar-cpp-nosig-plugin.jar.asc │ │ ├── sonar-cpp-plugin.jar.asc │ │ ├── sonar-cpp-unknownkey-plugin.jar.asc │ │ └── sonarsource-public.key │ ├── http/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── http/ │ │ │ ├── ApacheHttpClientAdapter.java │ │ │ ├── ApacheHttpResponse.java │ │ │ ├── ContextAttributes.java │ │ │ ├── HttpClient.java │ │ │ ├── HttpClientProvider.java │ │ │ ├── HttpConfig.java │ │ │ ├── HttpConnectionListener.java │ │ │ ├── RedirectInterceptor.java │ │ │ ├── RetryOnDemandStrategy.java │ │ │ ├── ThreadFactories.java │ │ │ ├── WebSocketClient.java │ │ │ ├── package-info.java │ │ │ └── ssl/ │ │ │ ├── CertificateStore.java │ │ │ ├── SslConfig.java │ │ │ └── package-info.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── http/ │ │ │ ├── HttpClientProviderTests.java │ │ │ └── WebSocketClientTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── plugin-api/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── plugin/ │ │ │ └── api/ │ │ │ ├── SonarLintRuntime.java │ │ │ ├── issue/ │ │ │ │ ├── NewInputFileEdit.java │ │ │ │ ├── NewQuickFix.java │ │ │ │ ├── NewSonarLintIssue.java │ │ │ │ ├── NewTextEdit.java │ │ │ │ └── package-info.java │ │ │ ├── module/ │ │ │ │ └── file/ │ │ │ │ ├── ModuleFileEvent.java │ │ │ │ ├── ModuleFileListener.java │ │ │ │ ├── ModuleFileSystem.java │ │ │ │ └── package-info.java │ │ │ └── package-info.java │ │ └── resources/ │ │ └── sonarlint-api-version.txt │ ├── plugin-commons/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ ├── com/ │ │ │ │ └── sonarsource/ │ │ │ │ └── plugins/ │ │ │ │ └── license/ │ │ │ │ └── api/ │ │ │ │ ├── LicensedPluginRegistration.java │ │ │ │ └── package-info.java │ │ │ └── org/ │ │ │ ├── sonar/ │ │ │ │ └── api/ │ │ │ │ ├── SonarQubeVersion.java │ │ │ │ └── package-info.java │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── plugin/ │ │ │ └── commons/ │ │ │ ├── ApiVersions.java │ │ │ ├── DataflowBugDetection.java │ │ │ ├── ExtensionInstaller.java │ │ │ ├── ExtensionUtils.java │ │ │ ├── LoadedPlugins.java │ │ │ ├── MultivalueProperty.java │ │ │ ├── PluginsLoadResult.java │ │ │ ├── PluginsLoader.java │ │ │ ├── api/ │ │ │ │ ├── SkipReason.java │ │ │ │ └── package-info.java │ │ │ ├── container/ │ │ │ │ ├── ClassDerivedBeanDefinition.java │ │ │ │ ├── ComponentKeys.java │ │ │ │ ├── Container.java │ │ │ │ ├── ExtensionContainer.java │ │ │ │ ├── LazyUnlessStartableStrategy.java │ │ │ │ ├── PriorityBeanFactory.java │ │ │ │ ├── SpringComponentContainer.java │ │ │ │ ├── SpringInitStrategy.java │ │ │ │ ├── StartableBeanPostProcessor.java │ │ │ │ ├── StartableContainer.java │ │ │ │ └── package-info.java │ │ │ ├── loading/ │ │ │ │ ├── PluginClassLoaderDef.java │ │ │ │ ├── PluginClassloaderFactory.java │ │ │ │ ├── PluginInfo.java │ │ │ │ ├── PluginInstancesLoader.java │ │ │ │ ├── PluginRequirementsCheckResult.java │ │ │ │ ├── SonarPluginManifest.java │ │ │ │ ├── SonarPluginRequirementsChecker.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── sonarapi/ │ │ │ ├── ConfigurationBridge.java │ │ │ ├── MapSettings.java │ │ │ ├── PluginContextImpl.java │ │ │ ├── SonarLintRuntimeImpl.java │ │ │ └── package-info.java │ │ └── test/ │ │ ├── java/ │ │ │ ├── com/ │ │ │ │ └── sonarsource/ │ │ │ │ └── plugins/ │ │ │ │ └── license/ │ │ │ │ └── api/ │ │ │ │ └── LicensedPluginRegistrationTests.java │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── plugin/ │ │ │ └── commons/ │ │ │ ├── ApiVersionsTests.java │ │ │ ├── ExtensionUtilsTests.java │ │ │ ├── MultivaluePropertyTests.java │ │ │ ├── SkipReasonTests.java │ │ │ ├── Utils.java │ │ │ ├── container/ │ │ │ │ ├── ComponentKeysTests.java │ │ │ │ ├── LazyUnlessStartableStrategyTests.java │ │ │ │ ├── PriorityBeanFactoryTests.java │ │ │ │ ├── SpringComponentContainerTests.java │ │ │ │ └── StartableBeanPostProcessorTests.java │ │ │ ├── loading/ │ │ │ │ ├── PluginClassloaderFactoryTests.java │ │ │ │ ├── PluginInfoTests.java │ │ │ │ ├── PluginInstancesLoaderTests.java │ │ │ │ ├── SonarPluginManifestTests.java │ │ │ │ └── SonarPluginRequirementsCheckerTests.java │ │ │ └── sonarapi/ │ │ │ └── MapSettingsTests.java │ │ ├── projects/ │ │ │ ├── .gitignore │ │ │ ├── README.txt │ │ │ ├── base-plugin/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── org/ │ │ │ │ └── sonar/ │ │ │ │ └── plugins/ │ │ │ │ └── base/ │ │ │ │ ├── BasePlugin.java │ │ │ │ └── api/ │ │ │ │ └── BaseApi.java │ │ │ ├── classloader-leak-plugin/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── org/ │ │ │ │ │ └── sonar/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── leak/ │ │ │ │ │ └── LeakPlugin.java │ │ │ │ └── resources/ │ │ │ │ └── Hello.txt │ │ │ ├── dependent-plugin/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── org/ │ │ │ │ └── sonar/ │ │ │ │ └── plugins/ │ │ │ │ └── dependent/ │ │ │ │ └── DependentPlugin.java │ │ │ └── pom.xml │ │ └── resources/ │ │ ├── SonarPluginManifestTests/ │ │ │ ├── plugin-with-jre-min.jar │ │ │ ├── plugin-with-nodejs-min.jar │ │ │ ├── plugin-with-require-plugins.jar │ │ │ └── plugin-without-jre-min.jar │ │ ├── logback-test.xml │ │ └── sonar-checkstyle-plugin-2.8.jar │ ├── pom.xml │ ├── rpc-impl/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── rpc/ │ │ │ │ └── impl/ │ │ │ │ ├── AbstractRpcServiceDelegate.java │ │ │ │ ├── AiAgentRpcServiceDelegate.java │ │ │ │ ├── AiCodeFixRpcServiceDelegate.java │ │ │ │ ├── AnalysisRpcServiceDelegate.java │ │ │ │ ├── BackendJsonRpcLauncher.java │ │ │ │ ├── BindingRpcServiceDelegate.java │ │ │ │ ├── ConfigurationRpcServiceDelegate.java │ │ │ │ ├── ConnectionRpcServiceDelegate.java │ │ │ │ ├── DependencyRiskRpcServiceDelegate.java │ │ │ │ ├── DogfoodingRpcServiceDelegate.java │ │ │ │ ├── FileRpcServiceDelegate.java │ │ │ │ ├── HotspotRpcServiceDelegate.java │ │ │ │ ├── IdeLabsRpcServiceDelegate.java │ │ │ │ ├── IssueRpcServiceDelegate.java │ │ │ │ ├── LogServiceDelegate.java │ │ │ │ ├── NewCodeRpcServiceDelegate.java │ │ │ │ ├── PluginRpcServiceDelegate.java │ │ │ │ ├── RpcClientLogOutput.java │ │ │ │ ├── RulesRpcServiceDelegate.java │ │ │ │ ├── SonarLintRpcClientLogbackAppender.java │ │ │ │ ├── SonarLintRpcServerImpl.java │ │ │ │ ├── SonarProjectBranchRpcServiceDelegate.java │ │ │ │ ├── TaintVulnerabilityTrackingRpcServiceDelegate.java │ │ │ │ ├── TaskProgressRpcServiceDelegate.java │ │ │ │ ├── TelemetryRpcServiceDelegate.java │ │ │ │ └── package-info.java │ │ │ └── resources/ │ │ │ └── logback.xml │ │ └── test/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── rpc/ │ │ └── impl/ │ │ ├── AnalysisServiceTests.java │ │ └── SonarLintRpcServerImplTests.java │ ├── rule-extractor/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── rule/ │ │ │ └── extractor/ │ │ │ ├── EmptyConfiguration.java │ │ │ ├── LegacyHotspotRuleDescriptionSectionsGenerator.java │ │ │ ├── NoopTempFolder.java │ │ │ ├── RuleDefinitionsLoader.java │ │ │ ├── RuleExtractionSettings.java │ │ │ ├── RuleSettings.java │ │ │ ├── RulesDefinitionExtractor.java │ │ │ ├── RulesDefinitionExtractorContainer.java │ │ │ ├── SecurityStandards.java │ │ │ ├── SonarLintRuleDefinition.java │ │ │ ├── SonarLintRuleDescriptionSection.java │ │ │ ├── SonarLintRuleParamDefinition.java │ │ │ ├── SonarLintRuleParamType.java │ │ │ └── package-info.java │ │ └── test/ │ │ ├── java/ │ │ │ ├── mediumtests/ │ │ │ │ └── RuleExtractorMediumTests.java │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── rule/ │ │ │ └── extractor/ │ │ │ ├── EmptyConfigurationTest.java │ │ │ ├── LegacyHotspotRuleDescriptionSectionsGeneratorTest.java │ │ │ ├── NoopTempFolderTest.java │ │ │ ├── SecurityStandardsTest.java │ │ │ └── SonarLintRuleDefinitionTests.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── server-api/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── serverapi/ │ │ │ │ ├── EndpointParams.java │ │ │ │ ├── ServerApi.java │ │ │ │ ├── ServerApiHelper.java │ │ │ │ ├── UrlUtils.java │ │ │ │ ├── authentication/ │ │ │ │ │ ├── AuthenticationApi.java │ │ │ │ │ ├── ValidateResponseDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── branches/ │ │ │ │ │ ├── ProjectBranchesApi.java │ │ │ │ │ ├── ServerBranch.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── component/ │ │ │ │ │ ├── Component.java │ │ │ │ │ ├── ComponentApi.java │ │ │ │ │ ├── SearchProjectResponse.java │ │ │ │ │ ├── SearchProjectResponseDto.java │ │ │ │ │ ├── ServerProject.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── developers/ │ │ │ │ │ ├── DevelopersApi.java │ │ │ │ │ ├── SearchEventsResponseDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── ForbiddenException.java │ │ │ │ │ ├── NetworkException.java │ │ │ │ │ ├── NotFoundException.java │ │ │ │ │ ├── ProjectNotFoundException.java │ │ │ │ │ ├── ServerErrorException.java │ │ │ │ │ ├── ServerRequestException.java │ │ │ │ │ ├── TooManyRequestsException.java │ │ │ │ │ ├── UnauthorizedException.java │ │ │ │ │ ├── UnexpectedBodyException.java │ │ │ │ │ ├── UnexpectedServerResponseException.java │ │ │ │ │ ├── UnsupportedServerException.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── features/ │ │ │ │ │ ├── Feature.java │ │ │ │ │ ├── FeaturesApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── fixsuggestions/ │ │ │ │ │ ├── AiCodeFixConfiguration.java │ │ │ │ │ ├── AiSuggestionRequestBodyDto.java │ │ │ │ │ ├── AiSuggestionResponseBodyDto.java │ │ │ │ │ ├── FixSuggestionsApi.java │ │ │ │ │ ├── OrganizationConfigsResponseDto.java │ │ │ │ │ ├── SuggestionFeatureEnablement.java │ │ │ │ │ ├── SupportedRulesResponseDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── hotspot/ │ │ │ │ │ ├── HotspotApi.java │ │ │ │ │ ├── ServerHotspot.java │ │ │ │ │ ├── ServerHotspotDetails.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── issue/ │ │ │ │ │ ├── IssueApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── newcode/ │ │ │ │ │ ├── NewCodeApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── organization/ │ │ │ │ │ ├── GetOrganizationsResponseDto.java │ │ │ │ │ ├── OrganizationApi.java │ │ │ │ │ ├── ServerOrganization.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── plugins/ │ │ │ │ │ ├── InstalledPluginsPayloadDto.java │ │ │ │ │ ├── PluginsApi.java │ │ │ │ │ ├── ServerPlugin.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── projectbindings/ │ │ │ │ │ ├── ProjectBindingsApi.java │ │ │ │ │ ├── SQCProjectBindingsResponse.java │ │ │ │ │ ├── SQCProjectBindingsResponseDto.java │ │ │ │ │ ├── SQSProjectBindingsResponse.java │ │ │ │ │ ├── SQSProjectBindingsResponseDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── push/ │ │ │ │ │ ├── IssueChangedEvent.java │ │ │ │ │ ├── PushApi.java │ │ │ │ │ ├── RuleSetChangedEvent.java │ │ │ │ │ ├── SecurityHotspotChangedEvent.java │ │ │ │ │ ├── SecurityHotspotClosedEvent.java │ │ │ │ │ ├── SecurityHotspotRaisedEvent.java │ │ │ │ │ ├── ServerHotspotEvent.java │ │ │ │ │ ├── SonarProjectEvent.java │ │ │ │ │ ├── SonarServerEvent.java │ │ │ │ │ ├── TaintVulnerabilityClosedEvent.java │ │ │ │ │ ├── TaintVulnerabilityRaisedEvent.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── parsing/ │ │ │ │ │ ├── EventParser.java │ │ │ │ │ ├── IssueChangedEventParser.java │ │ │ │ │ ├── RuleSetChangedEventParser.java │ │ │ │ │ ├── SecurityHotspotChangedEventParser.java │ │ │ │ │ ├── SecurityHotspotClosedEventParser.java │ │ │ │ │ ├── SecurityHotspotRaisedEventParser.java │ │ │ │ │ ├── TaintVulnerabilityClosedEventParser.java │ │ │ │ │ ├── TaintVulnerabilityRaisedEventParser.java │ │ │ │ │ ├── common/ │ │ │ │ │ │ ├── ImpactPayload.java │ │ │ │ │ │ ├── LocationPayload.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── qualityprofile/ │ │ │ │ │ ├── QualityProfile.java │ │ │ │ │ ├── QualityProfileApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── rules/ │ │ │ │ │ ├── RulesApi.java │ │ │ │ │ ├── ServerActiveRule.java │ │ │ │ │ ├── ServerRule.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── sca/ │ │ │ │ │ ├── GetIssuesReleasesResponse.java │ │ │ │ │ ├── GetScaEnablementResponse.java │ │ │ │ │ ├── ScaApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── settings/ │ │ │ │ │ ├── SettingsApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── source/ │ │ │ │ │ ├── SourceApi.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── stream/ │ │ │ │ │ ├── Event.java │ │ │ │ │ ├── EventBuffer.java │ │ │ │ │ ├── EventParser.java │ │ │ │ │ ├── EventStream.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── system/ │ │ │ │ │ ├── ServerStatusInfo.java │ │ │ │ │ ├── SystemApi.java │ │ │ │ │ ├── SystemStatusDto.java │ │ │ │ │ ├── ValidationResult.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── users/ │ │ │ │ │ ├── CurrentUserResponseDto.java │ │ │ │ │ ├── UsersApi.java │ │ │ │ │ └── package-info.java │ │ │ │ └── util/ │ │ │ │ ├── ProtobufUtil.java │ │ │ │ ├── ServerApiUtils.java │ │ │ │ └── package-info.java │ │ │ └── proto/ │ │ │ ├── sonarcloud/ │ │ │ │ └── ws-organizations.proto │ │ │ └── sonarqube/ │ │ │ ├── ws-commons.proto │ │ │ ├── ws-components.proto │ │ │ ├── ws-hotspots.proto │ │ │ ├── ws-issues.proto │ │ │ ├── ws-measures.proto │ │ │ ├── ws-projectbranches.proto │ │ │ ├── ws-qualityprofiles.proto │ │ │ ├── ws-rules.proto │ │ │ └── ws-settings.proto │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── serverapi/ │ │ │ ├── MockWebServerExtensionWithProtobuf.java │ │ │ ├── ServerApiHelperTests.java │ │ │ ├── authentication/ │ │ │ │ └── AuthenticationApiTests.java │ │ │ ├── branches/ │ │ │ │ ├── ProjectBranchesApiTests.java │ │ │ │ └── ServerBranchTests.java │ │ │ ├── component/ │ │ │ │ ├── ComponentApiTests.java │ │ │ │ └── ServerProjectTests.java │ │ │ ├── developers/ │ │ │ │ └── DevelopersApiTests.java │ │ │ ├── exception/ │ │ │ │ └── ProjectNotFoundExceptionTests.java │ │ │ ├── fixsuggestions/ │ │ │ │ └── FixSuggestionsApiTest.java │ │ │ ├── hotspot/ │ │ │ │ ├── HotspotApiTests.java │ │ │ │ └── ServerHotspotDetailsTests.java │ │ │ ├── issue/ │ │ │ │ └── IssueApiTests.java │ │ │ ├── newcode/ │ │ │ │ └── NewCodeApiTests.java │ │ │ ├── organization/ │ │ │ │ ├── OrganizationApiTests.java │ │ │ │ └── ServerOrganizationTests.java │ │ │ ├── plugins/ │ │ │ │ └── PluginsApiTests.java │ │ │ ├── projectbindings/ │ │ │ │ └── ProjectBindingsApiTests.java │ │ │ ├── push/ │ │ │ │ ├── PushApiTests.java │ │ │ │ └── parsing/ │ │ │ │ ├── SecurityHotspotChangedEventParserTest.java │ │ │ │ ├── SecurityHotspotClosedEventParserTest.java │ │ │ │ └── SecurityHotspotRaisedEventParserTest.java │ │ │ ├── qualityprofile/ │ │ │ │ └── QualityProfileApiTests.java │ │ │ ├── rules/ │ │ │ │ └── RulesApiTests.java │ │ │ ├── sca/ │ │ │ │ └── ScaApiTests.java │ │ │ ├── settings/ │ │ │ │ └── SettingsApiTests.java │ │ │ ├── stream/ │ │ │ │ └── EventStreamTests.java │ │ │ ├── users/ │ │ │ │ └── UsersApiTests.java │ │ │ └── util/ │ │ │ └── ProtobufUtilTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── update/ │ │ ├── component_tree.pb │ │ ├── empty_component_tree.pb │ │ └── empty_rules.pb │ ├── server-connection/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── serverconnection/ │ │ │ │ ├── AnalyzerConfiguration.java │ │ │ │ ├── AnalyzerConfigurationStorage.java │ │ │ │ ├── AnalyzerSettingsUpdateSummary.java │ │ │ │ ├── ConnectionStorage.java │ │ │ │ ├── DownloadException.java │ │ │ │ ├── DownloaderUtils.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── HotspotDownloader.java │ │ │ │ ├── IssueDownloader.java │ │ │ │ ├── IssueStorePaths.java │ │ │ │ ├── LocalStorageSynchronizer.java │ │ │ │ ├── Organization.java │ │ │ │ ├── OrganizationSynchronizer.java │ │ │ │ ├── ProjectBinding.java │ │ │ │ ├── ProjectBranches.java │ │ │ │ ├── ProjectBranchesStorage.java │ │ │ │ ├── RuleSet.java │ │ │ │ ├── ServerHotspotUpdater.java │ │ │ │ ├── ServerInfoSynchronizer.java │ │ │ │ ├── ServerIssueUpdater.java │ │ │ │ ├── ServerSettings.java │ │ │ │ ├── ServerUpdaterUtils.java │ │ │ │ ├── ServerVersionAndStatusChecker.java │ │ │ │ ├── Settings.java │ │ │ │ ├── SonarProjectStorage.java │ │ │ │ ├── SonarServerSettingsChangedEvent.java │ │ │ │ ├── StoredPlugin.java │ │ │ │ ├── StoredServerInfo.java │ │ │ │ ├── SynchronizationResult.java │ │ │ │ ├── TaintIssueDownloader.java │ │ │ │ ├── UserSynchronizer.java │ │ │ │ ├── VersionUtils.java │ │ │ │ ├── aicodefix/ │ │ │ │ │ ├── AiCodeFix.java │ │ │ │ │ ├── AiCodeFixFeatureEnablement.java │ │ │ │ │ ├── AiCodeFixRepository.java │ │ │ │ │ ├── AiCodeFixSettings.java │ │ │ │ │ ├── AiCodeFixSettingsSynchronizer.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── issues/ │ │ │ │ │ ├── FileLevelServerIssue.java │ │ │ │ │ ├── Findings.java │ │ │ │ │ ├── KnownFindingsRepository.java │ │ │ │ │ ├── LineLevelServerIssue.java │ │ │ │ │ ├── LocalOnlyIssuesRepository.java │ │ │ │ │ ├── RangeLevelServerIssue.java │ │ │ │ │ ├── ServerDependencyRisk.java │ │ │ │ │ ├── ServerFinding.java │ │ │ │ │ ├── ServerIssue.java │ │ │ │ │ ├── ServerTaintIssue.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── prefix/ │ │ │ │ │ ├── FileTreeMatcher.java │ │ │ │ │ ├── ReversePathTree.java │ │ │ │ │ └── package-info.java │ │ │ │ └── storage/ │ │ │ │ ├── EntityMapper.java │ │ │ │ ├── HotspotReviewStatusBinding.java │ │ │ │ ├── InstantBinding.java │ │ │ │ ├── IssueSeverityBinding.java │ │ │ │ ├── IssueStatusBinding.java │ │ │ │ ├── IssueTypeBinding.java │ │ │ │ ├── NewCodeDefinitionStorage.java │ │ │ │ ├── OrganizationStorage.java │ │ │ │ ├── PluginsStorage.java │ │ │ │ ├── ProjectServerIssueStore.java │ │ │ │ ├── ProjectStoragePaths.java │ │ │ │ ├── ProtobufFileUtil.java │ │ │ │ ├── RWLock.java │ │ │ │ ├── ServerFindingRepository.java │ │ │ │ ├── ServerFindingType.java │ │ │ │ ├── ServerInfoStorage.java │ │ │ │ ├── ServerIssueStoresManager.java │ │ │ │ ├── SmartNotificationsStorage.java │ │ │ │ ├── StorageException.java │ │ │ │ ├── StorageUtils.java │ │ │ │ ├── TarGzUtils.java │ │ │ │ ├── UpdateSummary.java │ │ │ │ ├── UserStorage.java │ │ │ │ ├── UuidBinding.java │ │ │ │ └── package-info.java │ │ │ └── proto/ │ │ │ └── sonarlint.proto │ │ └── test/ │ │ ├── java/ │ │ │ ├── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── sonarlint/ │ │ │ │ └── core/ │ │ │ │ └── serverconnection/ │ │ │ │ ├── AnalyzerConfigurationStorageTests.java │ │ │ │ ├── DownloadExceptionTests.java │ │ │ │ ├── FileUtilsTests.java │ │ │ │ ├── HotspotDownloaderTests.java │ │ │ │ ├── IssueDownloaderTests.java │ │ │ │ ├── IssueStorePathsTests.java │ │ │ │ ├── ProjectBindingTests.java │ │ │ │ ├── ProjectStoragePathsTests.java │ │ │ │ ├── ServerHotspotUpdaterTests.java │ │ │ │ ├── ServerInfoSynchronizerTests.java │ │ │ │ ├── ServerIssueUpdaterTests.java │ │ │ │ ├── ServerVersionAndStatusCheckerTests.java │ │ │ │ ├── StorageExceptionTests.java │ │ │ │ ├── TaintIssueDownloaderTests.java │ │ │ │ ├── UserSynchronizerTests.java │ │ │ │ ├── VersionUtilsTests.java │ │ │ │ ├── aicodefix/ │ │ │ │ │ └── AiCodeFixRepositoryTest.java │ │ │ │ ├── issues/ │ │ │ │ │ ├── KnownFindingsRepositoryTests.java │ │ │ │ │ ├── LocalOnlyIssuesRepositoryTests.java │ │ │ │ │ └── ServerIssueTests.java │ │ │ │ ├── prefix/ │ │ │ │ │ ├── FileTreeMatcherTests.java │ │ │ │ │ └── ReversePathTreeTests.java │ │ │ │ └── storage/ │ │ │ │ ├── EntityMapperTests.java │ │ │ │ ├── NewCodeDefinitionStorageTests.java │ │ │ │ ├── PluginsStorageTests.java │ │ │ │ ├── ProtobufFileUtilTests.java │ │ │ │ ├── ServerFindingRepositoryTests.java │ │ │ │ ├── ServerHotspotFixtures.java │ │ │ │ └── ServerIssueFixtures.java │ │ │ └── testutils/ │ │ │ └── MockWebServerExtensionWithProtobuf.java │ │ └── resources/ │ │ └── logback-test.xml │ └── telemetry/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── telemetry/ │ │ ├── InternalDebug.java │ │ ├── TelemetryAnalysisReportingCounter.java │ │ ├── TelemetryAnalyzerPerformance.java │ │ ├── TelemetryConnectionAttributes.java │ │ ├── TelemetryFindingsFilteredCounter.java │ │ ├── TelemetryFixSuggestionReceivedCounter.java │ │ ├── TelemetryFixSuggestionResolvedStatus.java │ │ ├── TelemetryHelpAndFeedbackCounter.java │ │ ├── TelemetryHttpClient.java │ │ ├── TelemetryLiveAttributes.java │ │ ├── TelemetryLocalStorage.java │ │ ├── TelemetryLocalStorageManager.java │ │ ├── TelemetryManager.java │ │ ├── TelemetryNotificationsCounter.java │ │ ├── TelemetryServerAttributes.java │ │ ├── TelemetryUtils.java │ │ ├── ToolCallCounter.java │ │ ├── common/ │ │ │ ├── TelemetryUserSetting.java │ │ │ └── package-info.java │ │ ├── gessie/ │ │ │ ├── GessieHttpClient.java │ │ │ ├── GessieService.java │ │ │ ├── event/ │ │ │ │ ├── GessieEvent.java │ │ │ │ ├── GessieMetadata.java │ │ │ │ ├── package-info.java │ │ │ │ └── payload/ │ │ │ │ ├── MessagePayload.java │ │ │ │ └── package-info.java │ │ │ └── package-info.java │ │ ├── measures/ │ │ │ └── payload/ │ │ │ ├── TelemetryMeasuresBuilder.java │ │ │ ├── TelemetryMeasuresDimension.java │ │ │ ├── TelemetryMeasuresPayload.java │ │ │ ├── TelemetryMeasuresValue.java │ │ │ ├── TelemetryMeasuresValueGranularity.java │ │ │ ├── TelemetryMeasuresValueType.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ └── payload/ │ │ ├── HotspotPayload.java │ │ ├── IssuePayload.java │ │ ├── ShareConnectedModePayload.java │ │ ├── ShowHotspotPayload.java │ │ ├── ShowIssuePayload.java │ │ ├── TaintVulnerabilitiesPayload.java │ │ ├── TelemetryAnalyzerPerformancePayload.java │ │ ├── TelemetryFixSuggestionPayload.java │ │ ├── TelemetryFixSuggestionResolvedPayload.java │ │ ├── TelemetryHelpAndFeedbackPayload.java │ │ ├── TelemetryNotificationsCounterPayload.java │ │ ├── TelemetryNotificationsPayload.java │ │ ├── TelemetryPayload.java │ │ ├── TelemetryRulesPayload.java │ │ ├── cayc/ │ │ │ ├── CleanAsYouCodePayload.java │ │ │ ├── NewCodeFocusPayload.java │ │ │ └── package-info.java │ │ └── package-info.java │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── telemetry/ │ │ ├── TelemetryHttpClientTests.java │ │ ├── TelemetryLocalStorageManagerTests.java │ │ ├── TelemetryLocalStorageTests.java │ │ ├── TelemetryManagerTests.java │ │ ├── TelemetryUtilsTests.java │ │ ├── gessie/ │ │ │ ├── GessieHttpClientTests.java │ │ │ └── event/ │ │ │ └── GessieMetadataTests.java │ │ └── payload/ │ │ ├── TelemetryMeasuresPayloadTests.java │ │ └── TelemetryPayloadTests.java │ └── resources/ │ └── response/ │ └── gessie/ │ └── GessieHttpClientTest/ │ ├── GessieRequest.json │ └── InvalidRequest.json ├── buildSrc/ │ ├── README.md │ └── maven-shade-ext-bnd-transformer/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── org/ │ └── sonarsource/ │ └── sonarlint/ │ └── maven/ │ └── shade/ │ └── ext/ │ └── ManifestBndTransformer.java ├── client/ │ ├── java-client-dependencies/ │ │ └── pom.xml │ ├── java-client-osgi/ │ │ ├── java-client-osgi-sources.bnd │ │ ├── java-client-osgi.bnd │ │ ├── pom.xml │ │ └── shared.bnd │ ├── java-client-utils/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── sonarlint/ │ │ │ └── core/ │ │ │ └── client/ │ │ │ └── utils/ │ │ │ ├── CleanCodeAttribute.java │ │ │ ├── CleanCodeAttributeCategory.java │ │ │ ├── ClientFileExclusions.java │ │ │ ├── ClientLogOutput.java │ │ │ ├── DateUtils.java │ │ │ ├── DependencyRiskTransitionStatus.java │ │ │ ├── GitUtils.java │ │ │ ├── HotspotStatus.java │ │ │ ├── ImpactSeverity.java │ │ │ ├── IssueResolutionStatus.java │ │ │ ├── Language.java │ │ │ ├── SoftwareQuality.java │ │ │ └── package-info.java │ │ └── test/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── client/ │ │ └── utils/ │ │ ├── CleanCodeAttributeCategoryTests.java │ │ ├── CleanCodeAttributeTests.java │ │ ├── ClientFileExclusionsTests.java │ │ ├── DateUtilsTests.java │ │ ├── DependencyRiskTransitionStatusTest.java │ │ ├── GitUtilsTests.java │ │ ├── HotspotStatusTest.java │ │ ├── ImpactSeverityTests.java │ │ ├── IssueResolutionStatusTest.java │ │ ├── LanguageTests.java │ │ └── SoftwareQualityTests.java │ ├── pom.xml │ └── rpc-java-client/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── rpc/ │ │ └── client/ │ │ ├── ClientJsonRpcLauncher.java │ │ ├── ConfigScopeNotFoundException.java │ │ ├── ConnectionNotFoundException.java │ │ ├── Sloop.java │ │ ├── SloopLauncher.java │ │ ├── SonarLintCancelChecker.java │ │ ├── SonarLintRpcClientDelegate.java │ │ ├── SonarLintRpcClientImpl.java │ │ └── package-info.java │ └── test/ │ └── java/ │ └── org/ │ └── sonarsource/ │ └── sonarlint/ │ └── core/ │ └── rpc/ │ └── client/ │ ├── SloopLauncherTests.java │ └── SonarLintRpcClientImplTest.java ├── doc/ │ └── analyzer_management.md ├── docs/ │ ├── PULL_REQUEST_TEMPLATE.md │ ├── contributing.md │ └── decisions/ │ ├── 0000-move-more-responsibilities-to-the-core.md │ ├── 0001-implement-binding-suggestions-in-the-core.md │ ├── 0002-manage-http-client-in-core.md │ └── template.md ├── its/ │ ├── README.md │ ├── plugins/ │ │ ├── custom-sensor-plugin/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── sonarsource/ │ │ │ │ └── plugins/ │ │ │ │ └── example/ │ │ │ │ ├── ExamplePlugin.java │ │ │ │ ├── FooLintRulesDefinition.java │ │ │ │ └── OneIssuePerLineSensor.java │ │ │ └── resources/ │ │ │ └── example/ │ │ │ └── foolint-rules.xml │ │ ├── global-extension-plugin/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── sonarsource/ │ │ │ └── plugins/ │ │ │ └── example/ │ │ │ ├── GlobalExtension.java │ │ │ ├── GlobalExtensionPlugin.java │ │ │ ├── GlobalLanguage.java │ │ │ ├── GlobalRulesDefinition.java │ │ │ ├── GlobalSensor.java │ │ │ └── GlobalSonarWayProfile.java │ │ └── java-custom-rules/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── sonar/ │ │ │ └── samples/ │ │ │ └── java/ │ │ │ ├── MyJavaFileCheckRegistrar.java │ │ │ ├── MyJavaRulesDefinition.java │ │ │ ├── MyJavaRulesPlugin.java │ │ │ ├── RulesList.java │ │ │ └── checks/ │ │ │ └── AvoidAnnotationRule.java │ │ └── resources/ │ │ └── org/ │ │ └── sonar/ │ │ └── l10n/ │ │ └── java/ │ │ └── rules/ │ │ └── java/ │ │ ├── AvoidAnnotation.html │ │ └── AvoidAnnotation.json │ ├── pom.xml │ └── tests/ │ ├── pom.xml │ ├── projects/ │ │ ├── sample-apex/ │ │ │ └── src/ │ │ │ └── file.cls │ │ ├── sample-c/ │ │ │ └── src/ │ │ │ ├── file.c │ │ │ └── foo.h │ │ ├── sample-cloudformation/ │ │ │ └── src/ │ │ │ └── sample.yaml │ │ ├── sample-cobol/ │ │ │ ├── copybooks/ │ │ │ │ ├── Attr.cpy │ │ │ │ ├── Custmas.cpy │ │ │ │ ├── Errparm.cpy │ │ │ │ └── MNTSET2.CPY │ │ │ └── src/ │ │ │ └── Custmnt2.cbl │ │ ├── sample-custom-secrets/ │ │ │ └── src/ │ │ │ └── file.md │ │ ├── sample-dbd/ │ │ │ └── src/ │ │ │ └── hello.py │ │ ├── sample-docker/ │ │ │ └── src/ │ │ │ └── Dockerfile │ │ ├── sample-global-extension/ │ │ │ └── src/ │ │ │ └── foo.glob │ │ ├── sample-go/ │ │ │ └── src/ │ │ │ └── sample.go │ │ ├── sample-java/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── foo/ │ │ │ └── Foo.java │ │ ├── sample-java-custom/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── foo/ │ │ │ └── Foo.java │ │ ├── sample-java-hotspot/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── foo/ │ │ │ └── Foo.java │ │ ├── sample-java-taint/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── foo/ │ │ │ ├── DbHelper.java │ │ │ └── Endpoint.java │ │ ├── sample-javascript/ │ │ │ └── src/ │ │ │ └── Person.js │ │ ├── sample-jcl/ │ │ │ └── GAM0VCDB.jcl │ │ ├── sample-kotlin/ │ │ │ └── src/ │ │ │ └── hello.kt │ │ ├── sample-kubernetes/ │ │ │ └── src/ │ │ │ └── sample.yaml │ │ ├── sample-language-mix/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── foo/ │ │ │ ├── Foo.java │ │ │ └── main.py │ │ ├── sample-misra/ │ │ │ └── foo.cpp │ │ ├── sample-php/ │ │ │ └── src/ │ │ │ └── Math.php │ │ ├── sample-python/ │ │ │ └── src/ │ │ │ └── hello.py │ │ ├── sample-ruby/ │ │ │ └── src/ │ │ │ └── hello.rb │ │ ├── sample-sca/ │ │ │ └── pom.xml │ │ ├── sample-scala/ │ │ │ └── src/ │ │ │ └── Hello.scala │ │ ├── sample-terraform/ │ │ │ └── src/ │ │ │ └── sample.tf │ │ ├── sample-tsql/ │ │ │ └── src/ │ │ │ └── file.tsql │ │ ├── sample-typescript/ │ │ │ ├── .gitignore │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── Person.ts │ │ │ └── tsconfig.json │ │ ├── sample-web/ │ │ │ └── src/ │ │ │ └── file.html │ │ └── sample-xml/ │ │ └── src/ │ │ └── foo.xml │ └── src/ │ └── test/ │ ├── java/ │ │ └── its/ │ │ ├── AbstractConnectedTests.java │ │ ├── FileExclusionTests.java │ │ ├── MockSonarLintRpcClientDelegate.java │ │ ├── SonarCloudTests.java │ │ ├── SonarQubeCommunityEditionTests.java │ │ ├── SonarQubeDeveloperEditionTests.java │ │ ├── SonarQubeEnterpriseEditionTests.java │ │ ├── StandaloneTests.java │ │ └── utils/ │ │ ├── AnalysisUtils.java │ │ ├── ItUtils.java │ │ ├── LogOnTestFailure.java │ │ ├── OrchestratorUtils.java │ │ ├── PluginLocator.java │ │ └── TestClientInputFile.java │ └── resources/ │ ├── apex-sonarlint.xml │ ├── c-sonarlint.xml │ ├── cloudformation-sonarlint.xml │ ├── cobol-sonarlint.xml │ ├── cpp-misra-sonarlint.xml │ ├── custom-secrets-sonarlint.xml │ ├── custom-sensor.xml │ ├── dbd-sonarlint.xml │ ├── docker-sonarlint.xml │ ├── global-extension.xml │ ├── go-sonarlint.xml │ ├── java-custom.xml │ ├── java-sonarlint-with-hotspot.xml │ ├── java-sonarlint-with-markdown.xml │ ├── java-sonarlint-with-taint.xml │ ├── java-sonarlint.xml │ ├── javascript-sonarlint.xml │ ├── jcl-sonarlint.xml │ ├── kotlin-sonarlint.xml │ ├── kubernetes-sonarlint.xml │ ├── php-sonarlint.xml │ ├── python-sonarlint.xml │ ├── ruby-sonarlint.xml │ ├── scala-sonarlint.xml │ ├── terraform-sonarlint.xml │ ├── tsql-sonarlint.xml │ ├── typescript-sonarlint.xml │ ├── web-sonarlint.xml │ └── xml-sonarlint.xml ├── medium-tests/ │ ├── pom.xml │ └── src/ │ └── test/ │ ├── java/ │ │ ├── mediumtest/ │ │ │ ├── BindingSuggestionsMediumTests.java │ │ │ ├── BindingTelemetryMediumTests.java │ │ │ ├── ConnectedHotspotMediumTests.java │ │ │ ├── ConnectedIssueExclusionsMediumTests.java │ │ │ ├── ConnectedIssueMediumTests.java │ │ │ ├── ConnectedStorageProblemsMediumTests.java │ │ │ ├── ConnectionSetupMediumTests.java │ │ │ ├── ConnectionSuggestionMediumTests.java │ │ │ ├── EffectiveRulesMediumTests.java │ │ │ ├── EmbeddedServerMediumTests.java │ │ │ ├── InitializationMediumTests.java │ │ │ ├── MCPServerConfigurationProviderMediumTests.java │ │ │ ├── NotebookLanguageMediumTests.java │ │ │ ├── SharedConnectedModeSettingsMediumTests.java │ │ │ ├── SonarLintTestHarnessMediumTests.java │ │ │ ├── StandaloneIssueMediumTests.java │ │ │ ├── StandaloneNoPluginMediumTests.java │ │ │ ├── StandaloneRulesMediumTests.java │ │ │ ├── TelemetryMediumTests.java │ │ │ ├── ai/ │ │ │ │ └── ide/ │ │ │ │ ├── AiAgentMediumTests.java │ │ │ │ └── AiHookMediumTests.java │ │ │ ├── analysis/ │ │ │ │ ├── ActiveRulesDumpingSensor.java │ │ │ │ ├── AnalysisCharsetMediumTests.java │ │ │ │ ├── AnalysisForcedByClientMediumTests.java │ │ │ │ ├── AnalysisMediumTests.java │ │ │ │ ├── AnalysisReadinessMediumTests.java │ │ │ │ ├── AnalysisSchedulingMediumTests.java │ │ │ │ ├── AnalysisTriggeringMediumTests.java │ │ │ │ ├── NodeJsMediumTests.java │ │ │ │ ├── PropertyDumpingSensor.java │ │ │ │ ├── RulesInConnectedModeMediumTests.java │ │ │ │ ├── SupportedFilePatternsMediumTests.java │ │ │ │ ├── ToggleAutomaticAnalysisMediumTests.java │ │ │ │ └── sensor/ │ │ │ │ ├── ThrowingSensorConstructor.java │ │ │ │ ├── WaitingCancellationSensor.java │ │ │ │ └── WaitingSensor.java │ │ │ ├── branch/ │ │ │ │ └── SonarProjectBranchMediumTests.java │ │ │ ├── connection/ │ │ │ │ ├── ConnectionGetAllProjectsMediumTests.java │ │ │ │ ├── ConnectionGetProjectNameByKeyMediumTests.java │ │ │ │ ├── ConnectionValidatorMediumTests.java │ │ │ │ └── OrganizationMediumTests.java │ │ │ ├── dogfooding/ │ │ │ │ └── DogfoodingRpcServiceMediumTests.java │ │ │ ├── file/ │ │ │ │ ├── ClientFileExclusionsMediumTests.java │ │ │ │ └── ConnectedFileExclusionsMediumTests.java │ │ │ ├── fixtures/ │ │ │ │ └── LocalOnlyIssueFixtures.java │ │ │ ├── gessie/ │ │ │ │ └── GessieIntegrationMediumTests.java │ │ │ ├── hotspots/ │ │ │ │ ├── HotspotCheckStatusChangePermittedMediumTests.java │ │ │ │ ├── HotspotEventsMediumTests.java │ │ │ │ ├── HotspotLocalDetectionSupportMediumTests.java │ │ │ │ ├── HotspotStatusChangeMediumTests.java │ │ │ │ ├── OpenHotspotInBrowserMediumTests.java │ │ │ │ └── OpenHotspotInIdeMediumTests.java │ │ │ ├── http/ │ │ │ │ ├── AuthenticationMediumTests.java │ │ │ │ ├── ProxyMediumTests.java │ │ │ │ ├── SslMediumTests.java │ │ │ │ └── TimeoutMediumTests.java │ │ │ ├── issues/ │ │ │ │ ├── AnalyzeFileListMediumTests.java │ │ │ │ ├── CheckAnticipatedStatusChangeSupportedMediumTests.java │ │ │ │ ├── CheckResolutionStatusChangePermittedMediumTests.java │ │ │ │ ├── IssueEventsMediumTests.java │ │ │ │ ├── IssuesStatusChangeMediumTests.java │ │ │ │ ├── LocalOnlyResolvedIssuesStorageMediumTests.java │ │ │ │ ├── OpenFixSuggestionInIdeMediumTests.java │ │ │ │ └── OpenIssueInIdeMediumTests.java │ │ │ ├── labs/ │ │ │ │ └── IdeLabsMediumTests.java │ │ │ ├── log/ │ │ │ │ └── LoggingMediumTests.java │ │ │ ├── monitoring/ │ │ │ │ ├── MonitoringMediumTests.java │ │ │ │ └── ThrowingPhpSensor.java │ │ │ ├── newcode/ │ │ │ │ └── NewCodeTelemetryMediumTests.java │ │ │ ├── plugin/ │ │ │ │ ├── OnDemandAnalyzersMediumTests.java │ │ │ │ └── PluginRpcServiceMediumTests.java │ │ │ ├── promotion/ │ │ │ │ ├── CampaignMediumTests.java │ │ │ │ └── ExtraEnabledLanguagesInConnectedModePromotionMediumTests.java │ │ │ ├── remediation/ │ │ │ │ └── aicodefix/ │ │ │ │ └── AiCodeFixMediumTest.java │ │ │ ├── rules/ │ │ │ │ ├── OkRulesDefinition.java │ │ │ │ ├── RuleEventsMediumTests.java │ │ │ │ ├── RuleExtractionMediumTests.java │ │ │ │ └── ThrowingRulesDefinition.java │ │ │ ├── sca/ │ │ │ │ ├── CheckDependencyRisksSupportedMediumTests.java │ │ │ │ ├── DependencyRiskStatusChangeMediumTests.java │ │ │ │ ├── DependencyRisksMediumTests.java │ │ │ │ └── OpenDependencyRiskInBrowserMediumTests.java │ │ │ ├── server/ │ │ │ │ └── events/ │ │ │ │ └── ServerSentEventsMediumTests.java │ │ │ ├── sloop/ │ │ │ │ ├── JreLocator.java │ │ │ │ ├── ProcessUtils.java │ │ │ │ ├── SloopDistLocator.java │ │ │ │ ├── SloopLauncherTests.java │ │ │ │ ├── SloopLauncherWithJreTests.java │ │ │ │ └── UnArchiveUtils.java │ │ │ ├── smartnotifications/ │ │ │ │ └── SmartNotificationsMediumTests.java │ │ │ ├── sonarcodecontext/ │ │ │ │ └── SonarCodeContextMediumTests.java │ │ │ ├── synchronization/ │ │ │ │ ├── BranchSpecificSynchronizationMediumTests.java │ │ │ │ ├── ConnectionSyncMediumTests.java │ │ │ │ ├── PluginSynchronizationMediumTests.java │ │ │ │ ├── RuleSetSynchronizationMediumTests.java │ │ │ │ ├── ServerInfoSynchronizationMediumTests.java │ │ │ │ ├── TaintVulnerabilitySynchronizationMediumTests.java │ │ │ │ └── UserSynchronizationMediumTests.java │ │ │ ├── taint/ │ │ │ │ └── vulnerabilities/ │ │ │ │ ├── TaintVulnerabilitiesMediumTests.java │ │ │ │ └── TaintVulnerabilityEventsMediumTests.java │ │ │ ├── tracking/ │ │ │ │ ├── IssueStreamingRulesDefinition.java │ │ │ │ ├── IssueStreamingSensor.java │ │ │ │ ├── IssueTrackingMediumTests.java │ │ │ │ └── SecurityHotspotTrackingMediumTests.java │ │ │ └── websockets/ │ │ │ └── WebSocketMediumTests.java │ │ └── utils/ │ │ ├── AnalysisUtils.java │ │ ├── JuliSLF4JDelegatingLog.java │ │ ├── MockWebServerExtensionWithProtobuf.java │ │ ├── OnDiskTestClientInputFile.java │ │ ├── PluginLocator.java │ │ ├── TestPlugin.java │ │ └── ThreadLeakDetector.java │ ├── projects/ │ │ ├── java-with-bytecode/ │ │ │ └── src/ │ │ │ └── Foo.java │ │ └── windows-shortcut/ │ │ ├── hello.py │ │ ├── hello.py.lnk │ │ └── hellp.py.fake.lnk │ └── resources/ │ ├── META-INF/ │ │ └── services/ │ │ ├── org.apache.juli.logging.Log │ │ └── org.junit.jupiter.api.extension.Extension │ ├── file-with-utf8-bom.js │ ├── logback-test.xml │ ├── response/ │ │ └── gessie/ │ │ └── GessieIntegrationMediumTests/ │ │ └── GessieRequest.json │ └── ssl/ │ ├── README.md │ ├── ca-client-auth.crt │ ├── ca-client-auth.key │ ├── ca.crt │ ├── ca.key │ ├── client.csr │ ├── client.key │ ├── client.p12 │ ├── client.pem │ ├── openssl-client-auth.conf │ ├── openssl.conf │ ├── server-with-client-ca.p12 │ ├── server.csr │ ├── server.jks │ ├── server.key │ ├── server.p12 │ ├── server.pem │ └── v3.ext ├── mise.toml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── report-aggregate/ │ └── pom.xml ├── rpc-protocol/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── rpc/ │ │ └── protocol/ │ │ ├── Lsp4jUtils.java │ │ ├── RpcErrorHandler.java │ │ ├── SingleThreadedMessageConsumer.java │ │ ├── SonarLintLauncherBuilder.java │ │ ├── SonarLintRpcClient.java │ │ ├── SonarLintRpcErrorCode.java │ │ ├── SonarLintRpcServer.java │ │ ├── adapter/ │ │ │ ├── CustomEitherAdapterFactory.java │ │ │ ├── DurationTypeAdapter.java │ │ │ ├── EitherCredentialsAdapterFactory.java │ │ │ ├── EitherProgressNotificationAdapterFactory.java │ │ │ ├── EitherRuleDescriptionAdapterFactory.java │ │ │ ├── EitherRuleDescriptionTabContentAdapterFactory.java │ │ │ ├── EitherSonarQubeSonarCloudConnectionAdapterFactory.java │ │ │ ├── EitherSonarQubeSonarCloudConnectionParamsAdapterFactory.java │ │ │ ├── EitherStandardOrMQRModeAdapterFactory.java │ │ │ ├── EitherTransientConnectionAdapterFactory.java │ │ │ ├── EitherTypeAdapter.java │ │ │ ├── InstantTypeAdapter.java │ │ │ ├── OffsetDateTimeAdapter.java │ │ │ ├── PathTypeAdapter.java │ │ │ ├── UriTypeAdapter.java │ │ │ ├── UuidTypeAdapter.java │ │ │ └── package-info.java │ │ ├── backend/ │ │ │ ├── ai/ │ │ │ │ ├── AiAgent.java │ │ │ │ ├── AiAgentRpcService.java │ │ │ │ ├── GetHookScriptContentParams.java │ │ │ │ ├── GetHookScriptContentResponse.java │ │ │ │ ├── GetRuleFileContentParams.java │ │ │ │ ├── GetRuleFileContentResponse.java │ │ │ │ └── package-info.java │ │ │ ├── analysis/ │ │ │ │ ├── AnalysisRpcService.java │ │ │ │ ├── AnalyzeFileListParams.java │ │ │ │ ├── AnalyzeFilesAndTrackParams.java │ │ │ │ ├── AnalyzeFilesResponse.java │ │ │ │ ├── AnalyzeFullProjectParams.java │ │ │ │ ├── AnalyzeOpenFilesParams.java │ │ │ │ ├── AnalyzeVCSChangedFilesParams.java │ │ │ │ ├── DidChangeAnalysisPropertiesParams.java │ │ │ │ ├── DidChangeAutomaticAnalysisSettingParams.java │ │ │ │ ├── DidChangeClientNodeJsPathParams.java │ │ │ │ ├── DidChangePathToCompileCommandsParams.java │ │ │ │ ├── ForceAnalyzeResponse.java │ │ │ │ ├── GetAutoDetectedNodeJsResponse.java │ │ │ │ ├── GetForcedNodeJsResponse.java │ │ │ │ ├── GetSupportedFilePatternsParams.java │ │ │ │ ├── GetSupportedFilePatternsResponse.java │ │ │ │ ├── NodeJsDetailsDto.java │ │ │ │ ├── ShouldUseEnterpriseCSharpAnalyzerParams.java │ │ │ │ ├── ShouldUseEnterpriseCSharpAnalyzerResponse.java │ │ │ │ └── package-info.java │ │ │ ├── binding/ │ │ │ │ ├── BindingRpcService.java │ │ │ │ ├── GetBindingSuggestionParams.java │ │ │ │ ├── GetSharedConnectedModeConfigFileParams.java │ │ │ │ ├── GetSharedConnectedModeConfigFileResponse.java │ │ │ │ └── package-info.java │ │ │ ├── branch/ │ │ │ │ ├── DidVcsRepositoryChangeParams.java │ │ │ │ ├── GetMatchedSonarProjectBranchParams.java │ │ │ │ ├── GetMatchedSonarProjectBranchResponse.java │ │ │ │ ├── SonarProjectBranchRpcService.java │ │ │ │ └── package-info.java │ │ │ ├── config/ │ │ │ │ ├── ConfigurationRpcService.java │ │ │ │ ├── binding/ │ │ │ │ │ ├── BindingConfigurationDto.java │ │ │ │ │ ├── BindingMode.java │ │ │ │ │ ├── BindingSuggestionDto.java │ │ │ │ │ ├── BindingSuggestionOrigin.java │ │ │ │ │ ├── DidUpdateBindingParams.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ └── scope/ │ │ │ │ ├── ConfigurationScopeDto.java │ │ │ │ ├── DidAddConfigurationScopesParams.java │ │ │ │ ├── DidRemoveConfigurationScopeParams.java │ │ │ │ └── package-info.java │ │ │ ├── connection/ │ │ │ │ ├── ConnectionRpcService.java │ │ │ │ ├── GetConnectionSuggestionsResponse.java │ │ │ │ ├── GetMCPServerConfigurationParams.java │ │ │ │ ├── GetMCPServerConfigurationResponse.java │ │ │ │ ├── auth/ │ │ │ │ │ ├── HelpGenerateUserTokenParams.java │ │ │ │ │ ├── HelpGenerateUserTokenResponse.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── common/ │ │ │ │ │ ├── TransientSonarCloudConnectionDto.java │ │ │ │ │ ├── TransientSonarQubeConnectionDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── config/ │ │ │ │ │ ├── DidChangeCredentialsParams.java │ │ │ │ │ ├── DidUpdateConnectionsParams.java │ │ │ │ │ ├── SonarCloudConnectionConfigurationDto.java │ │ │ │ │ ├── SonarQubeConnectionConfigurationDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── org/ │ │ │ │ │ ├── FuzzySearchUserOrganizationsParams.java │ │ │ │ │ ├── FuzzySearchUserOrganizationsResponse.java │ │ │ │ │ ├── GetOrganizationParams.java │ │ │ │ │ ├── GetOrganizationResponse.java │ │ │ │ │ ├── ListUserOrganizationsParams.java │ │ │ │ │ ├── ListUserOrganizationsResponse.java │ │ │ │ │ ├── OrganizationDto.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── package-info.java │ │ │ │ ├── projects/ │ │ │ │ │ ├── FuzzySearchProjectsParams.java │ │ │ │ │ ├── FuzzySearchProjectsResponse.java │ │ │ │ │ ├── GetAllProjectsParams.java │ │ │ │ │ ├── GetAllProjectsResponse.java │ │ │ │ │ ├── GetProjectNamesByKeyParams.java │ │ │ │ │ ├── GetProjectNamesByKeyResponse.java │ │ │ │ │ ├── SonarProjectDto.java │ │ │ │ │ └── package-info.java │ │ │ │ └── validate/ │ │ │ │ ├── ValidateConnectionParams.java │ │ │ │ ├── ValidateConnectionResponse.java │ │ │ │ └── package-info.java │ │ │ ├── dogfooding/ │ │ │ │ ├── DogfoodingRpcService.java │ │ │ │ ├── IsDogfoodingEnvironmentResponse.java │ │ │ │ └── package-info.java │ │ │ ├── file/ │ │ │ │ ├── DidCloseFileParams.java │ │ │ │ ├── DidOpenFileParams.java │ │ │ │ ├── DidUpdateFileSystemParams.java │ │ │ │ ├── FileRpcService.java │ │ │ │ ├── FileStatusDto.java │ │ │ │ ├── GetFilesStatusParams.java │ │ │ │ ├── GetFilesStatusResponse.java │ │ │ │ └── package-info.java │ │ │ ├── hotspot/ │ │ │ │ ├── ChangeHotspotStatusParams.java │ │ │ │ ├── CheckLocalDetectionSupportedParams.java │ │ │ │ ├── CheckLocalDetectionSupportedResponse.java │ │ │ │ ├── CheckStatusChangePermittedParams.java │ │ │ │ ├── CheckStatusChangePermittedResponse.java │ │ │ │ ├── HotspotRpcService.java │ │ │ │ ├── HotspotStatus.java │ │ │ │ ├── OpenHotspotInBrowserParams.java │ │ │ │ └── package-info.java │ │ │ ├── initialize/ │ │ │ │ ├── BackendCapability.java │ │ │ │ ├── ClientConstantInfoDto.java │ │ │ │ ├── HttpConfigurationDto.java │ │ │ │ ├── InitializeParams.java │ │ │ │ ├── JsTsRequirementsDto.java │ │ │ │ ├── LanguageSpecificRequirements.java │ │ │ │ ├── OmnisharpRequirementsDto.java │ │ │ │ ├── SonarCloudAlternativeEnvironmentDto.java │ │ │ │ ├── SonarQubeCloudRegionDto.java │ │ │ │ ├── SslConfigurationDto.java │ │ │ │ ├── TelemetryClientConstantAttributesDto.java │ │ │ │ ├── TelemetryMigrationDto.java │ │ │ │ └── package-info.java │ │ │ ├── issue/ │ │ │ │ ├── AddIssueCommentParams.java │ │ │ │ ├── ChangeIssueStatusParams.java │ │ │ │ ├── CheckAnticipatedStatusChangeSupportedParams.java │ │ │ │ ├── CheckAnticipatedStatusChangeSupportedResponse.java │ │ │ │ ├── CheckStatusChangePermittedParams.java │ │ │ │ ├── CheckStatusChangePermittedResponse.java │ │ │ │ ├── EffectiveIssueDetailsDto.java │ │ │ │ ├── GetEffectiveIssueDetailsParams.java │ │ │ │ ├── GetEffectiveIssueDetailsResponse.java │ │ │ │ ├── IssueRpcService.java │ │ │ │ ├── ReopenAllIssuesForFileParams.java │ │ │ │ ├── ReopenAllIssuesForFileResponse.java │ │ │ │ ├── ReopenIssueParams.java │ │ │ │ ├── ReopenIssueResponse.java │ │ │ │ ├── ResolutionStatus.java │ │ │ │ └── package-info.java │ │ │ ├── labs/ │ │ │ │ ├── IdeLabsRpcService.java │ │ │ │ ├── JoinIdeLabsProgramParams.java │ │ │ │ ├── JoinIdeLabsProgramResponse.java │ │ │ │ └── package-info.java │ │ │ ├── log/ │ │ │ │ ├── LogLevel.java │ │ │ │ ├── LogRpcService.java │ │ │ │ ├── SetLogLevelParams.java │ │ │ │ └── package-info.java │ │ │ ├── newcode/ │ │ │ │ ├── GetNewCodeDefinitionParams.java │ │ │ │ ├── GetNewCodeDefinitionResponse.java │ │ │ │ ├── NewCodeRpcService.java │ │ │ │ └── package-info.java │ │ │ ├── plugin/ │ │ │ │ ├── ArtifactSourceDto.java │ │ │ │ ├── GetPluginStatusesParams.java │ │ │ │ ├── GetPluginStatusesResponse.java │ │ │ │ ├── PluginRpcService.java │ │ │ │ ├── PluginStateDto.java │ │ │ │ ├── PluginStatusDto.java │ │ │ │ └── package-info.java │ │ │ ├── progress/ │ │ │ │ ├── CancelTaskParams.java │ │ │ │ ├── TaskProgressRpcService.java │ │ │ │ └── package-info.java │ │ │ ├── remediation/ │ │ │ │ └── aicodefix/ │ │ │ │ ├── AiCodeFixRpcService.java │ │ │ │ ├── SuggestFixChangeDto.java │ │ │ │ ├── SuggestFixParams.java │ │ │ │ ├── SuggestFixResponse.java │ │ │ │ └── package-info.java │ │ │ ├── rules/ │ │ │ │ ├── EffectiveRuleDetailsDto.java │ │ │ │ ├── EffectiveRuleParamDto.java │ │ │ │ ├── GetEffectiveRuleDetailsParams.java │ │ │ │ ├── GetEffectiveRuleDetailsResponse.java │ │ │ │ ├── GetStandaloneRuleDescriptionParams.java │ │ │ │ ├── GetStandaloneRuleDescriptionResponse.java │ │ │ │ ├── ImpactDto.java │ │ │ │ ├── ListAllStandaloneRulesDefinitionsResponse.java │ │ │ │ ├── RuleContextualSectionDto.java │ │ │ │ ├── RuleContextualSectionWithDefaultContextKeyDto.java │ │ │ │ ├── RuleDefinitionDto.java │ │ │ │ ├── RuleDescriptionTabDto.java │ │ │ │ ├── RuleMonolithicDescriptionDto.java │ │ │ │ ├── RuleNonContextualSectionDto.java │ │ │ │ ├── RuleParamDefinitionDto.java │ │ │ │ ├── RuleParamType.java │ │ │ │ ├── RuleSplitDescriptionDto.java │ │ │ │ ├── RulesRpcService.java │ │ │ │ ├── StandaloneRuleConfigDto.java │ │ │ │ ├── UpdateStandaloneRulesConfigurationParams.java │ │ │ │ ├── VulnerabilityProbability.java │ │ │ │ └── package-info.java │ │ │ ├── sca/ │ │ │ │ ├── ChangeDependencyRiskStatusParams.java │ │ │ │ ├── CheckDependencyRiskSupportedParams.java │ │ │ │ ├── CheckDependencyRiskSupportedResponse.java │ │ │ │ ├── DependencyRiskRpcService.java │ │ │ │ ├── DependencyRiskTransition.java │ │ │ │ ├── ListAllDependencyRisksResponse.java │ │ │ │ ├── OpenDependencyRiskInBrowserParams.java │ │ │ │ └── package-info.java │ │ │ ├── telemetry/ │ │ │ │ ├── GetStatusResponse.java │ │ │ │ ├── TelemetryRpcService.java │ │ │ │ └── package-info.java │ │ │ └── tracking/ │ │ │ ├── AffectedPackageDto.java │ │ │ ├── DependencyRiskDto.java │ │ │ ├── ListAllParams.java │ │ │ ├── ListAllResponse.java │ │ │ ├── RecommendationDetailsDto.java │ │ │ ├── TaintVulnerabilityDto.java │ │ │ ├── TaintVulnerabilityTrackingRpcService.java │ │ │ ├── TextRangeWithHashDto.java │ │ │ └── package-info.java │ │ ├── client/ │ │ │ ├── OpenUrlInBrowserParams.java │ │ │ ├── analysis/ │ │ │ │ ├── DidChangeAnalysisReadinessParams.java │ │ │ │ ├── DidDetectSecretParams.java │ │ │ │ ├── FileEditDto.java │ │ │ │ ├── GetFileExclusionsParams.java │ │ │ │ ├── GetFileExclusionsResponse.java │ │ │ │ ├── GetInferredAnalysisPropertiesParams.java │ │ │ │ ├── GetInferredAnalysisPropertiesResponse.java │ │ │ │ ├── QuickFixDto.java │ │ │ │ ├── RawIssueDto.java │ │ │ │ ├── RawIssueFlowDto.java │ │ │ │ ├── RawIssueLocationDto.java │ │ │ │ ├── TextEditDto.java │ │ │ │ └── package-info.java │ │ │ ├── binding/ │ │ │ │ ├── AssistBindingParams.java │ │ │ │ ├── AssistBindingResponse.java │ │ │ │ ├── GetBindingSuggestionsResponse.java │ │ │ │ ├── NoBindingSuggestionFoundParams.java │ │ │ │ ├── SuggestBindingParams.java │ │ │ │ └── package-info.java │ │ │ ├── branch/ │ │ │ │ ├── DidChangeMatchedSonarProjectBranchParams.java │ │ │ │ ├── MatchProjectBranchParams.java │ │ │ │ ├── MatchProjectBranchResponse.java │ │ │ │ ├── MatchSonarProjectBranchParams.java │ │ │ │ ├── MatchSonarProjectBranchResponse.java │ │ │ │ └── package-info.java │ │ │ ├── connection/ │ │ │ │ ├── AssistCreatingConnectionParams.java │ │ │ │ ├── AssistCreatingConnectionResponse.java │ │ │ │ ├── ConnectionSuggestionDto.java │ │ │ │ ├── GetConnectionSuggestionsParams.java │ │ │ │ ├── GetCredentialsParams.java │ │ │ │ ├── GetCredentialsResponse.java │ │ │ │ ├── SonarCloudConnectionParams.java │ │ │ │ ├── SonarCloudConnectionSuggestionDto.java │ │ │ │ ├── SonarQubeConnectionParams.java │ │ │ │ ├── SonarQubeConnectionSuggestionDto.java │ │ │ │ ├── SuggestConnectionParams.java │ │ │ │ └── package-info.java │ │ │ ├── embeddedserver/ │ │ │ │ ├── EmbeddedServerStartedParams.java │ │ │ │ └── package-info.java │ │ │ ├── event/ │ │ │ │ ├── DidReceiveServerHotspotEvent.java │ │ │ │ └── package-info.java │ │ │ ├── fix/ │ │ │ │ ├── ChangesDto.java │ │ │ │ ├── FileEditDto.java │ │ │ │ ├── FixSuggestionDto.java │ │ │ │ ├── LineRangeDto.java │ │ │ │ ├── ShowFixSuggestionParams.java │ │ │ │ └── package-info.java │ │ │ ├── fs/ │ │ │ │ ├── GetBaseDirParams.java │ │ │ │ ├── GetBaseDirResponse.java │ │ │ │ ├── ListFilesParams.java │ │ │ │ ├── ListFilesResponse.java │ │ │ │ └── package-info.java │ │ │ ├── hotspot/ │ │ │ │ ├── HotspotDetailsDto.java │ │ │ │ ├── RaiseHotspotsParams.java │ │ │ │ ├── RaisedHotspotDto.java │ │ │ │ ├── ShowHotspotParams.java │ │ │ │ └── package-info.java │ │ │ ├── http/ │ │ │ │ ├── CheckServerTrustedParams.java │ │ │ │ ├── CheckServerTrustedResponse.java │ │ │ │ ├── GetProxyPasswordAuthenticationParams.java │ │ │ │ ├── GetProxyPasswordAuthenticationResponse.java │ │ │ │ ├── ProxyDto.java │ │ │ │ ├── SelectProxiesParams.java │ │ │ │ ├── SelectProxiesResponse.java │ │ │ │ ├── X509CertificateDto.java │ │ │ │ └── package-info.java │ │ │ ├── info/ │ │ │ │ ├── GetClientLiveInfoResponse.java │ │ │ │ └── package-info.java │ │ │ ├── issue/ │ │ │ │ ├── FileEditDto.java │ │ │ │ ├── IssueDetailsDto.java │ │ │ │ ├── IssueFlowDto.java │ │ │ │ ├── IssueLocationDto.java │ │ │ │ ├── QuickFixDto.java │ │ │ │ ├── RaiseIssuesParams.java │ │ │ │ ├── RaisedFindingDto.java │ │ │ │ ├── RaisedIssueDto.java │ │ │ │ ├── ShowIssueParams.java │ │ │ │ ├── TextEditDto.java │ │ │ │ └── package-info.java │ │ │ ├── log/ │ │ │ │ ├── LogLevel.java │ │ │ │ ├── LogParams.java │ │ │ │ └── package-info.java │ │ │ ├── message/ │ │ │ │ ├── MessageActionItem.java │ │ │ │ ├── MessageType.java │ │ │ │ ├── ShowMessageParams.java │ │ │ │ ├── ShowMessageRequestParams.java │ │ │ │ ├── ShowMessageRequestResponse.java │ │ │ │ ├── ShowSoonUnsupportedMessageParams.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── plugin/ │ │ │ │ ├── DidChangePluginStatusesParams.java │ │ │ │ ├── DidSkipLoadingPluginParams.java │ │ │ │ ├── DidUpdatePluginsParams.java │ │ │ │ └── package-info.java │ │ │ ├── progress/ │ │ │ │ ├── ProgressEndNotification.java │ │ │ │ ├── ProgressUpdateNotification.java │ │ │ │ ├── ReportProgressParams.java │ │ │ │ ├── StartProgressParams.java │ │ │ │ └── package-info.java │ │ │ ├── promotion/ │ │ │ │ ├── PromoteExtraEnabledLanguagesInConnectedModeParams.java │ │ │ │ └── package-info.java │ │ │ ├── sca/ │ │ │ │ ├── DidChangeDependencyRisksParams.java │ │ │ │ └── package-info.java │ │ │ ├── smartnotification/ │ │ │ │ ├── ShowSmartNotificationParams.java │ │ │ │ └── package-info.java │ │ │ ├── sync/ │ │ │ │ ├── DidSynchronizeConfigurationScopeParams.java │ │ │ │ ├── InvalidTokenParams.java │ │ │ │ └── package-info.java │ │ │ ├── taint/ │ │ │ │ └── vulnerability/ │ │ │ │ ├── DidChangeTaintVulnerabilitiesParams.java │ │ │ │ └── package-info.java │ │ │ └── telemetry/ │ │ │ ├── AcceptedBindingSuggestionParams.java │ │ │ ├── AddQuickFixAppliedForRuleParams.java │ │ │ ├── AddReportedRulesParams.java │ │ │ ├── AiSuggestionSource.java │ │ │ ├── AnalysisDoneOnSingleLanguageParams.java │ │ │ ├── AnalysisReportingTriggeredParams.java │ │ │ ├── AnalysisReportingType.java │ │ │ ├── DevNotificationsClickedParams.java │ │ │ ├── FindingsFilteredParams.java │ │ │ ├── FixSuggestionResolvedParams.java │ │ │ ├── FixSuggestionStatus.java │ │ │ ├── HelpAndFeedbackClickedParams.java │ │ │ ├── IdeLabsExternalLinkClickedParams.java │ │ │ ├── IdeLabsFeedbackLinkClickedParams.java │ │ │ ├── McpTransportMode.java │ │ │ ├── McpTransportModeUsedParams.java │ │ │ ├── TelemetryClientLiveAttributesResponse.java │ │ │ ├── ToggleIdeLabsEnablementParams.java │ │ │ ├── ToolCalledParams.java │ │ │ └── package-info.java │ │ ├── common/ │ │ │ ├── CleanCodeAttribute.java │ │ │ ├── CleanCodeAttributeCategory.java │ │ │ ├── ClientFileDto.java │ │ │ ├── Either.java │ │ │ ├── FlowDto.java │ │ │ ├── ImpactSeverity.java │ │ │ ├── IssueSeverity.java │ │ │ ├── Language.java │ │ │ ├── LocationDto.java │ │ │ ├── MQRModeDetails.java │ │ │ ├── RuleType.java │ │ │ ├── SoftwareQuality.java │ │ │ ├── SonarCloudRegion.java │ │ │ ├── StandardModeDetails.java │ │ │ ├── TextRangeDto.java │ │ │ ├── TokenDto.java │ │ │ ├── UsernamePasswordDto.java │ │ │ └── package-info.java │ │ └── package-info.java │ └── test/ │ └── java/ │ └── org/ │ └── sonarsource/ │ └── sonarlint/ │ └── core/ │ └── rpc/ │ └── protocol/ │ ├── backend/ │ │ ├── config/ │ │ │ └── binding/ │ │ │ └── DidUpdateBindingParamsTests.java │ │ ├── connection/ │ │ │ └── projects/ │ │ │ └── SonarProjectDtoTest.java │ │ └── initialize/ │ │ ├── InitializeParamsTests.java │ │ └── TelemetryClientConstantAttributesDtoTests.java │ ├── client/ │ │ └── binding/ │ │ └── AssistBindingParamsTests.java │ └── common/ │ ├── EitherTests.java │ ├── TokenDtoTests.java │ └── UsernamePasswordDtoTests.java ├── spec/ │ ├── README.adoc │ ├── connected_mode/ │ │ ├── README.adoc │ │ ├── binding_suggestion.adoc │ │ └── synchronization/ │ │ ├── README.adoc │ │ ├── overview.adoc │ │ └── pull_synchronization.adoc │ ├── glossary.adoc │ ├── issue_tracking.adoc │ ├── rpc/ │ │ └── json_rpc.adoc │ └── smart_notifications.adoc ├── test-utils/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── sonarsource/ │ │ └── sonarlint/ │ │ └── core/ │ │ └── test/ │ │ └── utils/ │ │ ├── ProtobufUtils.java │ │ ├── SonarLintBackendFixture.java │ │ ├── SonarLintTestRpcServer.java │ │ ├── junit5/ │ │ │ ├── SonarLintTest.java │ │ │ ├── SonarLintTestHarness.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ ├── plugins/ │ │ │ ├── Plugin.java │ │ │ ├── SonarPluginBuilder.java │ │ │ ├── package-info.java │ │ │ └── src/ │ │ │ ├── DefaultPlugin.java │ │ │ ├── DefaultRulesDefinition.java │ │ │ ├── DefaultSensor.java │ │ │ └── package-info.java │ │ ├── server/ │ │ │ ├── ServerFixture.java │ │ │ ├── package-info.java │ │ │ ├── sse/ │ │ │ │ ├── SSEServer.java │ │ │ │ ├── SSEServlet.java │ │ │ │ └── package-info.java │ │ │ └── websockets/ │ │ │ ├── ContextListener.java │ │ │ ├── RequestListener.java │ │ │ ├── ServletAwareConfig.java │ │ │ ├── WebSocketConnection.java │ │ │ ├── WebSocketConnectionRepository.java │ │ │ ├── WebSocketEndpoint.java │ │ │ ├── WebSocketRequest.java │ │ │ ├── WebSocketServer.java │ │ │ └── package-info.java │ │ └── storage/ │ │ ├── AiCodeFixFixtures.java │ │ ├── ConfigurationScopeStorageFixture.java │ │ ├── ProjectStorageFixture.java │ │ ├── ServerDependencyRiskFixtures.java │ │ ├── ServerIssueFixtures.java │ │ ├── ServerSecurityHotspotFixture.java │ │ ├── ServerTaintIssueFixtures.java │ │ ├── StorageFixture.java │ │ ├── TestDatabase.java │ │ └── package-info.java │ └── test/ │ └── java/ │ └── org/ │ └── sonarsource/ │ └── sonarlint/ │ └── core/ │ └── test/ │ └── utils/ │ └── SonarLintTestRpcServerTest.java └── third-party-licenses.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.pb binary ================================================ FILE: .github/CODEOWNERS ================================================ .github/CODEOWNERS @SonarSource/remediation-ide-experience-squad ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>SonarSource/renovate-config:ide-xp-team" ] } ================================================ FILE: .github/workflows/PullRequestClosed.yml ================================================ name: Pull Request Closed on: pull_request: types: [closed] jobs: PullRequestClosed_job: name: Pull Request Closed runs-on: sonar-xs-public permissions: id-token: write pull-requests: read # For external PR, ticket should be moved manually if: | github.event.pull_request.head.repo.full_name == github.repository steps: - id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/kv/data/jira user | JIRA_USER; development/kv/data/jira token | JIRA_TOKEN; - uses: sonarsource/gh-action-lt-backlog/PullRequestClosed@v2 with: github-token: ${{secrets.GITHUB_TOKEN}} jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} ================================================ FILE: .github/workflows/PullRequestCreated.yml ================================================ name: Pull Request Created on: pull_request: types: ["opened"] jobs: PullRequestCreated_job: name: Pull Request Created runs-on: sonar-xs-public permissions: id-token: write # For external PR, ticket should be created manually if: | github.event.pull_request.head.repo.full_name == github.repository steps: - id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN; development/kv/data/jira user | JIRA_USER; development/kv/data/jira token | JIRA_TOKEN; - uses: sonarsource/gh-action-lt-backlog/PullRequestCreated@v2 with: github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }} jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} jira-project: SLCORE ================================================ FILE: .github/workflows/RequestReview.yml ================================================ name: Request review on: pull_request: types: ["review_requested"] jobs: RequestReview_job: name: Request review runs-on: sonar-xs-public permissions: id-token: write # For external PR, ticket should be moved manually if: | github.event.pull_request.head.repo.full_name == github.repository steps: - id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN; development/kv/data/jira user | JIRA_USER; development/kv/data/jira token | JIRA_TOKEN; - uses: sonarsource/gh-action-lt-backlog/RequestReview@v2 with: github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }} jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} ================================================ FILE: .github/workflows/SubmitReview.yml ================================================ name: Submit Review on: pull_request_review: types: [submitted] jobs: SubmitReview_job: name: Submit Review runs-on: sonar-xs-public permissions: id-token: write pull-requests: read # For external PR, ticket should be moved manually if: | github.event.pull_request.head.repo.full_name == github.repository && (github.event.review.state == 'changes_requested' || github.event.review.state == 'approved') steps: - id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/kv/data/jira user | JIRA_USER; development/kv/data/jira token | JIRA_TOKEN; - uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2 with: github-token: ${{secrets.GITHUB_TOKEN}} jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - master - branch-* - dogfood-* pull_request: merge_group: workflow_dispatch: concurrency: group: >- ${{ github.workflow }}- ${{ github.event.pull_request.base.ref || 'push' }}- ${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: CACHE_BACKEND: s3 jobs: build-number: outputs: BUILD_NUMBER: ${{ steps.build-number.outputs.BUILD_NUMBER }} runs-on: sonar-xs-public name: Get build number permissions: id-token: write steps: - uses: SonarSource/ci-github-actions/get-build-number@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 id: build-number build: runs-on: sonar-xs-public needs: build-number name: Build permissions: id-token: write contents: write env: BUILD_NUMBER: ${{ needs.build-number.outputs.BUILD_NUMBER }} outputs: build_number: ${{ steps.build.outputs.build_number }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 with: version: 2026.4.11 - uses: SonarSource/ci-github-actions/build-maven@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 id: build with: sonar-platform: none deploy-pull-request: true artifactory-reader-role: private-reader artifactory-deployer-role: qa-deployer maven-args: -T 1C -P dist-no-arch,dist-windows_x64,dist-linux_x64,dist-linux_aarch64,dist-macosx_x64,dist-macosx_aarch64 -Dmaven.test.skip=true -Dsonar.skip=true - name: Config Maven (cache setup) run: | mvn -B -e -V -Pits dependency:go-offline # populate cache including ITs deps too test-linux: needs: [ build-number, build ] runs-on: sonar-m-public name: Test (Linux, Sonar Next) permissions: id-token: write contents: write env: BUILD_NUMBER: ${{ needs.build-number.outputs.BUILD_NUMBER }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 with: version: 2026.4.11 - name: Vault id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/kv/data/next url | NEXT_URL; development/kv/data/next token | NEXT_TOKEN; - name: Cache Sonar Scanner artifacts id: sonar-scanner-cache uses: SonarSource/ci-github-actions/cache@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 with: path: ~/.sonar/cache key: sonar-scanner-${{ runner.os }} - uses: SonarSource/ci-github-actions/config-maven@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 id: config with: artifactory-reader-role: private-reader - name: Run tests env: SONAR_HOST_URL: ${{ fromJSON(steps.secrets.outputs.vault).NEXT_URL }} SONAR_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).NEXT_TOKEN }} PROJECT_VERSION: ${{ steps.config.outputs.project-version }} SCANNER_VERSION: 5.1.0.4751 PULL_REQUEST: ${{ github.event.pull_request.number || '' }} run: | mvn -B -Pcoverage -Dcommercial verify maven_goals=("org.sonarsource.scanner.maven:sonar-maven-plugin:${SCANNER_VERSION}:sonar") sonar_props=("-Dsonar.host.url=${SONAR_HOST_URL}" "-Dsonar.token=${SONAR_TOKEN}") sonar_props+=("-Dsonar.projectVersion=${CURRENT_VERSION}") sonar_props+=("-Dsonar.coverage.jacoco.aggregateXmlReportPaths=${{ github.workspace }}/report-aggregate/target/site/jacoco-aggregate/jacoco.xml") echo "Maven command: mvn ${maven_goals[*]} ${sonar_props[*]}" mvn -B "${maven_goals[@]}" "${sonar_props[@]}" - name: Generate test report on failure if: failure() uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 with: name: QA Linux Test Report reporter: java-junit path: '**/target/surefire-reports/TEST-*.xml,**/target/failsafe-reports/*.xml' list-suites: failed list-tests: failed fail-on-empty: false - name: Upload failure diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: linux-test-report path: | **/target/surefire-reports/** **/target/failsafe-reports/** test-windows: needs: [ build-number, build ] runs-on: github-windows-latest-m name: Test (Windows) permissions: id-token: write contents: write env: BUILD_NUMBER: ${{ needs.build-number.outputs.BUILD_NUMBER }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 with: version: 2026.4.11 - uses: SonarSource/ci-github-actions/config-maven@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 id: config with: artifactory-reader-role: private-reader - name: Run tests env: MAVEN_OPTS: -Xmx4g PROJECT_VERSION: ${{ steps.config.outputs.project-version }} run: | mvn -B -Dcommercial verify - name: Generate test report on failure if: failure() uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 with: name: QA Windows Test Report reporter: java-junit path: '**/target/surefire-reports/TEST-*.xml,**/target/failsafe-reports/*.xml' list-suites: failed list-tests: failed fail-on-empty: false - name: Upload failure diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: windows-test-report path: | **/target/surefire-reports/** **/target/failsafe-reports/** qa: needs: [ build-number, build ] runs-on: sonar-m-public name: QA (${{ matrix.name }}) permissions: id-token: write contents: write env: BUILD_NUMBER: ${{ needs.build-number.outputs.BUILD_NUMBER }} strategy: fail-fast: false matrix: include: - name: SonarCloudEU sq_version: SonarCloudEU category: "-Dgroups=SonarCloud" sc: true sc_token_path: sonarcloud-it region: EU - name: SonarCloudUS sq_version: SonarCloudUS category: "-Dgroups=SonarCloud" sc: true sc_token_path: sonarcloud-it-US region: US - name: SQDogfood sq_version: DEV category: "-DexcludedGroups=SonarCloud" - name: SQLatest sq_version: LATEST_RELEASE category: "-DexcludedGroups=SonarCloud" - name: SQLts99 sq_version: "LATEST_RELEASE[9.9]" category: "-DexcludedGroups=SonarCloud" customOrchestratorJavaVersion: 17 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 with: version: 2026.4.11 - name: Setup Java ${{ matrix.customOrchestratorJavaVersion }} for Orchestrator if: matrix.customOrchestratorJavaVersion run: | mise install java@${{ matrix.customOrchestratorJavaVersion }} echo "ORCHESTRATOR_JAVA_HOME=$(mise where java@${{ matrix.customOrchestratorJavaVersion }})" >> "$GITHUB_ENV" - name: Compute month key #Avoid caching for DEV since it is frequently changing if: ${{ matrix.sc != true && matrix.sq_version != 'DEV' }} id: month shell: bash run: | THIS_MONTH="$(date +%Y-%m)" echo "month=${THIS_MONTH}" >> "$GITHUB_OUTPUT" ORCHESTRATOR_HOME="${GITHUB_WORKSPACE}/orchestrator/${THIS_MONTH}" echo "ORCHESTRATOR_HOME=${ORCHESTRATOR_HOME}" >> "$GITHUB_ENV" echo "Create dir ${ORCHESTRATOR_HOME} if needed" mkdir -p "${ORCHESTRATOR_HOME}" - uses: SonarSource/ci-github-actions/cache@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 if: ${{ matrix.sc != true && matrix.sq_version != 'DEV' }} with: path: ${{ github.workspace }}/orchestrator/${{ steps.month.outputs.month }} key: cache-${{ runner.os }}-${{ steps.month.outputs.month }}-${{ matrix.name }} # Use matrix name to differentiate caches - name: Vault (SonarCloud IT token) if: ${{ matrix.sc == true }} id: secrets-sc uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/team/sonarlint/kv/data/${{ matrix.sc_token_path }} token | SONARCLOUD_IT_TOKEN; - name: Vault (GITHUB Token) id: secrets-gh uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/github/token/licenses-ro token | GITHUB_TOKEN; - uses: SonarSource/ci-github-actions/config-maven@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 with: artifactory-reader-role: private-reader - name: Run QA if: ${{ github.event_name == 'pull_request' || github.ref_name == github.event.repository.default_branch || startsWith(github.ref_name, 'branch-') || startsWith(github.ref_name, 'dogfood-on-') }} env: MAVEN_OPTS: -Xmx4g SONARCLOUD_IT_TOKEN: ${{ steps.secrets-sc.outputs.vault && fromJSON(steps.secrets-sc.outputs.vault).SONARCLOUD_IT_TOKEN || '' }} SONARCLOUD_REGION: ${{ matrix.sc && matrix.region || '' }} GITHUB_TOKEN: ${{ fromJSON(steps.secrets-gh.outputs.vault).GITHUB_TOKEN }} SONAR_SEARCH_JAVAADDITIONALOPTS: -XX:-UseContainerSupport SONAR_WEB_JAVAADDITIONALOPTS: -XX:-UseContainerSupport SONAR_CE_JAVAADDITIONALOPTS: -XX:-UseContainerSupport run: | mvn -f its/pom.xml -Dsonar.runtimeVersion=${{ matrix.sq_version }} ${{ matrix.category }} verify surefire-report:report - name: Generate QA test report on failure if: failure() uses: dorny/test-reporter@2dcf091ad558da2cabf16f6b423e02cd078c937a with: name: QA ${{ matrix.name }} Test Report reporter: java-junit path: '**/target/surefire-reports/TEST-*.xml,**/target/failsafe-reports/*.xml' list-suites: failed list-tests: failed fail-on-empty: false - name: Upload failure diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: qa-test-report ${{ matrix.name }} path: | **/target/surefire-reports/** **/target/failsafe-reports/** - name: debug if: failure() shell: bash run: | echo "=== Listing surefire-reports contents ===" find ./its/tests/target/surefire-reports -type f || true echo "=== Checking if directory is empty ===" [ -d ./its/tests/target/surefire-reports ] && ls -la ./its/tests/target/surefire-reports/ || echo "Directory doesn't exist" - name: Inspect Orchestrator Cache if: always() shell: bash run: | echo "=== Listing orchestrator cache contents ===" CACHE_DIR="${{ github.workspace }}/orchestrator/${{ steps.month.outputs.month }}" if [ -d "${CACHE_DIR}" ]; then echo "Directory exists: ${CACHE_DIR}" ls -lah "${CACHE_DIR}" echo "" echo "=== Detailed file tree ===" find "${CACHE_DIR}" -type f -ls || true else echo "Directory does not exist: ${CACHE_DIR}" fi promote: needs: [ build-number, build, qa, test-linux, test-windows ] runs-on: sonar-xs-public name: Promote permissions: id-token: write contents: write env: BUILD_NUMBER: ${{ needs.build-number.outputs.BUILD_NUMBER }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: SonarSource/ci-github-actions/promote@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 with: promote-pull-request: true ================================================ FILE: .github/workflows/full-release.yml ================================================ name: Full release on: workflow_dispatch: inputs: short-description: description: 'A short description for the release ticket' required: true type: string branch: description: 'The branch from which to release.' required: false default: 'master' type: string jobs: release: name: Release uses: SonarSource/release-github-actions/.github/workflows/ide-automated-release.yml@6f94d0d49b2bb6d324f3110ef078e3a5bd95604e # 1.5.4 if: always() && !failure() && !cancelled() permissions: statuses: read id-token: write contents: write actions: write pull-requests: write with: jira-project-key: "SLCORE" project-name: "SonarLint Core" short-description: ${{ inputs.short-description }} branch: ${{ inputs.branch }} bump-version: name: Create a PR to bump version runs-on: sonar-xs-public needs: - release permissions: contents: write # write for peter-evans/create-pull-request, read for actions/checkout pull-requests: write # write for peter-evans/create-pull-request steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - name: bump-version env: NEW_VERSION: ${{ needs.release.outputs.new-version }} run: | mvn versions:set -DgenerateBackupPoms=false -DnewVersion="$NEW_VERSION-SNAPSHOT" - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: author: ${{ github.actor }} <${{ github.actor }}> commit-message: Prepare next development iteration title: Prepare next development iteration branch: bot/bump-project-version branch-suffix: timestamp delete-branch: true base: master draft: true # so that we don't actually need to re-open the PR and just make it ready for review reviewers: ${{ github.actor }} body: Bump the version for the new iteration. Created by release automation. ================================================ FILE: .github/workflows/notify-failure.yml ================================================ name: Notify Failure on: workflow_run: workflows: [ "Build" ] types: - completed branches: - master permissions: id-token: write jobs: notify: runs-on: sonar-xs-public name: Send Slack Notification if: ${{ github.event.workflow_run.conclusion == 'failure' }} steps: - name: Vault Secrets id: secrets uses: SonarSource/vault-action-wrapper@c154b4a417b51cb98dd71137f49bf20e77c56820 # 3.4.0 with: secrets: | development/kv/data/slack token | SLACK_BOT_TOKEN; - name: Slack Notification on Failure uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: method: chat.postMessage token: ${{ fromJSON(steps.secrets.outputs.vault).SLACK_BOT_TOKEN }} # Slack channel squad-devex-flow-interrupts payload: | channel: C0ADM04C59A text: "Workflow failed in ${{ github.repository }}" blocks: - type: "section" text: type: "mrkdwn" text: ":x: *Repository:* ${{ github.repository }}\n*Branch:* ${{ github.event.workflow_run.head_branch }}\n*Workflow:* ${{ github.event.workflow_run.name }}\n*Run:* <${{ github.event.workflow_run.html_url }}|#${{ github.event.workflow_run.run_number }}>" ================================================ FILE: .github/workflows/releasability.yml ================================================ name: Releasability Status on: workflow_run: workflows: [ "Build" ] # Name must match the name of the build workflow types: [ completed ] branches: - master - branch-* jobs: releasability-status: name: Releasability status runs-on: sonar-xs-public permissions: id-token: write statuses: write contents: read if: github.event.workflow_run.conclusion == 'success' steps: - uses: SonarSource/gh-action_releasability/releasability-status@52f09917764eac5a80045d103bfa91e7eaf0c8d6 # 3.0.5 with: optional_checks: "Jira" env: GITHUB_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/release.yml ================================================ name: sonar-release on: workflow_dispatch: inputs: version: type: string description: Version required: true releaseId: type: string description: Release ID required: true dryRun: type: boolean description: Flag to enable the dry-run execution default: false required: false jobs: release: permissions: id-token: write contents: write uses: SonarSource/gh-action_release/.github/workflows/main.yaml@4ac0f4304e2858f8144ac48037bb135b5fdac1ad # 6.7.1 with: version: ${{ inputs.version }} releaseId: ${{ inputs.releaseId }} dryRun: ${{ inputs.dryRun }} publishToBinaries: false mavenCentralSync: true # Slack channel squad-devex-private slackChannel: C02E6L5C01H ================================================ FILE: .github/workflows/shadow_scans.yml ================================================ name: Shadow scans on: schedule: # Run the workflow every day at 04:00 UTC - cron: '0 4 * * *' workflow_dispatch: env: CACHE_BACKEND: s3 jobs: scan: runs-on: sonar-xs-public name: Scan on shadow platforms permissions: id-token: write contents: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 with: version: 2026.4.11 - uses: SonarSource/ci-github-actions/build-maven@23d3a5700259f9890438851083904c6d5e87ff4e # 1.3.34 # dogfood with: maven-args: -Dcommercial -Dsonar.coverage.jacoco.xmlReportPaths=${{ github.workspace }}/report-aggregate/target/site/jacoco-aggregate/jacoco.xml run-shadow-scans: true artifactory-reader-role: private-reader artifactory-deployer-role: qa-deployer scanner-java-opts: '-Xmx2g' - name: Run IRIS sync uses: SonarSource/unified-dogfooding-actions/run-iris@v1 with: primary_project_key: "org.sonarsource.sonarlint.core:sonarlint-core-parent" primary_platform: "Next" shadow1_project_key: "org.sonarsource.sonarlint.core:sonarlint-core-parent" shadow1_platform: "SQC-EU" shadow2_project_key: "org.sonarsource.sonarlint.core:sonarlint-core-parent" shadow2_platform: "SQC-US" ================================================ FILE: .gitignore ================================================ # The following should be moved in related sub-directories server/sonar-web/src/main/webapp/stylesheets/sonar-colorizer.css server/sonar-web/src/main/webapp/deploy/plugins server/sonar-web/src/main/webapp/deploy/bootstrap server/sonar-web/src/main/webapp/deploy/maven/org server/sonar-web/src/main/webapp/WEB-INF/log/ server/sonar-web/src/main/webapp/deploy/*.jar server/sonar-web/src/main/webapp/deploy/jdbc-driver.txt # ---- Javadoc docs.tar # ---- Maven target/ dependency-reduced-pom.xml # ---- IntelliJ IDEA *.iws *.iml *.ipr .idea/ # ---- Eclipse .classpath .project .settings .externalToolBuilders # ---- Mac OS X .DS_Store Icon? # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # ---- Windows # Windows image file caches Thumbs.db # Folder config file Desktop.ini # ---- Sonar .sonar/ .scannerwork # scripts patches, they are local to each developer scripts/patches/*.sh scripts/patches/*license*.txt # Visual Studio .vs # Run configurations .run ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.13/apache-maven-3.9.13-bin.zip ================================================ FILE: .sonarlint/connectedMode.json ================================================ { "sonarQubeUri": "https://next.sonarqube.com/sonarqube", "projectKey": "org.sonarsource.sonarlint.core:sonarlint-core-parent" } ================================================ FILE: API_CHANGES.md ================================================ # 10.48 ## New features * Add `label` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStateDto`. * Add `label` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.ArtifactSourceDto`. * Add `language` field to `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto`. * Add `serverVersion` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto`. * Contains the version of the SonarQube Server that provided the plugin (e.g. `"10.8.1"`). * Non-null only when `source` is `SONARQUBE_SERVER`; `null` for all other sources (embedded, SonarQube Cloud, unavailable). * Introduce two new telemetry notification methods to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService` to track usage of the "Supported Languages" panel: * `supportedLanguagesPanelOpened` - call this each time the user opens the "Supported Languages" panel. * `supportedLanguagesPanelCtaClicked` - call this each time the user clicks the "set up connection/binding" CTA button in the "Supported Languages" panel. # 10.46 ## New features * Introduce `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginRpcService`, accessible via `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getPluginService`. * Use `getPluginStatuses` to populate the "Supported Languages" panel with the full list of known analyzer statuses (one entry per language, including unsupported ones). * Pass a `configurationScopeId` to get statuses in the context of its bound connection, or `null` for standalone statuses. * Each `org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto` contains: plugin name, state (`org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStateDto`), source (`org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.ArtifactSourceDto`), actual version, and overridden version if applicable. * Add a new `didChangePluginStatuses` notification to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * Implement it to keep the "Supported Languages" panel up to date after the initial load. * Called when plugin statuses change, e.g. after a sync with a connection or when a connection is removed. # 10.44 ## Breaking changes * Add `closedByUser` flag to `org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestResponse`. It must be set to `true` if the `showMessageRequest` was explicitly closed by the user (e.g. via clicking X on the notification) # 10.43 ## Breaking changes * Remove the flight recorder feature. This should be handled by clients # 10.42 * Add `org.sonarsource.sonarlint.core.rpc.client.Sloop.getPid` to return the pid of the backend process. * Add a new `PROMOTIONAL_CAMPAIGNS` capability in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. Enables promotional campaign notifications and tracking user responses to not show again or postpone showing them. Data about shown notifications and user responses is saved separately for each IDE in the similar manner to telemetry in path: `{sonarUserHome}/campaigns/{productKey}/campaigns` * Add a new `KIRO` value to `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent` enum. # 10.39 ## Breaking changes * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService.getDependencyRiskDetails` method and associated DTOs. # 10.38 ## New features * Add a new `GESSIE_TELEMETRY` capability in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. Enables sending data to Gessie (Generic Event System) alongside previous telemetry implementation. # 10.37 ## Deprecation * Deprecate 4-parameter constructor and remove deprecation of 2-parameter one of `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams`. Move back to an old constructor as not all IDEs were able to provide all data to a new one. * Remove Deprecation from `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService#addedManualBindings` method. It should be used again for manual binding events instead of parametrized `didUpdateBinding`. ## New features * Add ide labs flags to `org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse`. * Introduce 2 new methods to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService` to record Ide Labs telemetry: `externalLinkClicked`, `feedbackLinkClicked`. * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService.acceptedBindingSuggestion`. It should be used to for bindings created based on suggestions and pass `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin` instead of parametrized `didUpdateBinding`. # 10.36 ## Breaking Changes * `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAssistedIdeRpcService` has been renamed to `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgentService`. * `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAssitedIde` has been renamed to `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent`. * `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getAiAssistedIdeRpcService` has been renamed to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getAiAgentService`. * Replace `VSCODE` and `VSCODE_INSIDERS` with `GITHUB_COPILOT` in `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent` enum. * This better reflects that the distinction is about the AI agent (GitHub Copilot), not the IDE ## New features * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.IdeLabsRpcService` service and a `joinIdeLabsProgram` method. * Use it to allow users to join the SonarQube for IDE Labs program * The method accepts user email and IDE name as parameters # 10.35 ## Breaking changes * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.FeatureFlagsDto` class. * Remove unused methods from `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService`: * `getGlobalStandaloneConfiguration` * `getGlobalConnectedConfiguration` * `getAnalysisConfig` * `getRuleDetails` ## New features * Add a new `CONTEXT_GENERATION` value in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. This is only accessible in dogfooding environments, and should be enabled in AI-related environments. * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.log.LogRpcService` service, with a new `setLogLevel` method. This allows clients to dynamically change the logging level. * Introduce a new constructor in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams`, that accepts a `LogLevel` parameter. ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` previous constructor. Please use the new one and provide the log level. # 10.33 ## New features * Add a new endpoint `/sonarlint/api/analysis/automatic/config` in the embedded server to globally disable or enable the automatic analysis. * This endpoint should be used by external clients such as MCP servers. * Add a new endpoint `/sonarlint/api/analysis/files` in our embedded server to analyze a list of files and return the issues, hotspots and taints found. * This endpoint should be used by external clients such as MCP servers. * Add a new `getMCPServerConfiguration` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService` * It accepts `connectionId` and `token` as parameters * It returns JSON string containing MCP server settings (without the `sonarqube` parent item) * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAssistedIdeRpcService` service and a `getRuleFileContent` method. * Use it to retrieve the content of the rule file to write to provide guidance to the agent when using the SonarQube MCP server. * Introduce an RPC notification `embeddedServerStarted` in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` * It is sent by the backend to notify the client that the embedded server has started * It contains the embedded server port * Example usage is by the MCP Server to establish the bridge connection * Add a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService.mcpIntegrationEnabled` method. * Should only be used by SonarQube MCP Server when integration with SQ:IDE is enabled and valid # 10.31 ## New features * Add a new `FLIGHT_RECORDER` value in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. Important note: the `MONITORING` capability is also required by this feature. * Add a new optional backend-to-client notification `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#flightRecorderStarted`. Clients can implement this notification to inform end users about a starting flight recorder session. * Add a new service to the backend API: `org.sonarsource.sonarlint.core.rpc.protocol.backend.flightrecorder.FlightRecordingRpcService` can be used to interact with the flight recorder (e.g. to capture a thread dump of the current backend process) * Add a new `CURRENT_FILE_ANALYSIS_TYPE` to the `org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingType` enum. This value can be used when reporting telemetry for forced analysis of currently open file. # 10.29 ## New features * Clients can now access more granular origin information via the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto`, `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.connection.ConnectionSuggestionDto` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.AssistBindingParams`. * Added `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingMode` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams` and allowed clients to provide this information when calling `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService#didUpdateBinding` which will trigger telemetry events for binding updates. Clients no longer need to call separate telemetry methods while adding bindings. ## Potential breaking changes * Clients are normally not expected to use following constructors directly, but if they do perhaps in their tests, it's a potential breaking change. * Removed the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto` constructor with `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto#isFromSharedConfiguration` parameter. Clients should use the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto#origin` parameter if it is used in their tests. `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.SHARED_CONFIGURATION` can be used if it was true, otherwise it wouldn't matter which other origin used, they can default to `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.PROJECT_NAME` * Removed the `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto` constructor with `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto#isFromSharedConfiguration` parameter. Clients should use the `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto#origin` parameter if it is used in their tests. `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.SHARED_CONFIGURATION` can be used if it was true, otherwise it wouldn't matter which other origin used, they can default to `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.PROJECT_NAME` * Removed the `org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams` constructor with `org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams#isFromSharedConfiguration` parameter. Clients should use the `org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams#origin` parameter if it is used in their tests. `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.SHARED_CONFIGURATION` can be used if it was true, otherwise it wouldn't matter which other origin used, they can default to `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.PROJECT_NAME` ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto#isFromSharedConfiguration` method since it is not used anymore. Use `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto#getOrigin` instead. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto#isFromSharedConfiguration` method since it is not used anymore. Use `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto#getOrigin` instead. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.AssistBindingParams#isFromSharedConfiguration` method since it is not used anymore. Use `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.AssistBindingParams#getOrigin` instead. * Deprecate `isFromSharedConfiguration` parameter from `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.AssistBindingParams`. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.AssistBindingParams` constructor. Use the other one. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams` constructor. Use the other one. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService#addedManualBindings` method. This will be automatically handled by passing the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingMode` to the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams` constructor during the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService#didUpdateBinding` call. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService#addedImportedBindings` method. This will be automatically handled by passing the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingMode` to the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams` constructor during the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService#didUpdateBinding` call. * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService#addedAutomaticBindings` method. This will be automatically handled by passing the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingMode` to the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams` constructor during the `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService#didUpdateBinding` call. # 10.28 ## Breaking changes * Fields `vulnerabilityId` and `description` from `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.GetDependencyRiskDetailsResponse` are now nullable. * Dependency risks of type `PROHIBITED_LICENSE` do not have a vulnerability ID or description. ## New features * Add `TEXT` language to `org.sonarsource.sonarlint.core.commons.api.SonarLanguage` * Enabling this language allows detecting [text issues](https://rules.sonarsource.com/text/) * Add `GITHUBACTIONS` language to `org.sonarsource.sonarlint.core.commons.api.SonarLanguage` * Enabling this language allows detecting [issues on GitHub Actions](https://rules.sonarsource.com/githubactions/) * Add new method `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService#checkSupported` to check if dependency risks are supported by the server * It returns the reason in case the server does not support dependency risks ## SCA * Introduce new fields `vulnerabilityId` and `cvssScore` in `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskDto` * The `vulnerabilityId` is a unique identifier for the vulnerability such as `CVE-1234`, and the `cvssScore` is the Common Vulnerability Scoring System score for the vulnerability * They are null in case the dependency risk is of type `PROHIBITED_LICENSE` # 10.27 ## Breaking changes * Merge `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskTrackingRpcService` into `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService`. * Rename `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.didChangeScaIssues` to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.didChangeDependencyRisks`. ## New features * Allow changing status of dependency risks (SCA issues) via `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService.changeStatus`. * Required parameters are `configScopeId`, `dependencyRiskKey` and `transition`. * If transition is `ACCEPT` or `SAFE`, a `comment` field is mandatory * Allow clients to open dependency risk in browser * Introduce `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService.openDependencyRiskInBrowser` that accepts `configScopeId` and `dependencyRiskKey` (UUID) parameters * Allow clients to record interactions with dependency risks in telemetry * Introduce `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService.dependencyRiskInvestigatedLocally` method * Add a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService.getDependencyRiskDetails`. # 10.26 ## New features * Add a new `SCA_SYNCHRONIZATION` value in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ScaIssueTrackingRpcService` service and a `listAll` method * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.didChangeScaIssues` notification. # 10.25 ## Breaking changes * Add a new `ISSUE_STREAMING` value in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. ## New features * Add a new optional parameter to `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams` to track SonarQube Cloud account creation through token generation. ## File exclusions * The RPC client method `org.sonarsource.sonarlint.core.rpc.protocol.backend.file.getFilesStatus` previously returned information exclusively about server exclusions. It now includes the same exclusion criteria as used during the analysis (client exclusions, gitignore, etc.). # 10.23 ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#matchProjectBranch` method since it is not used anymore. # 10.22 ## New features * Add a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService.toolCalled` method. * Add a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService.getConnectionSuggestions` method. * This method is used to get connection suggestions for a given configuration scope. * The method should only be used when neither connection nor binding exists for a given configuration scope. * If a connection already exists, a `org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.BindingRpcService.getBindingSuggestions` should be used instead # 10.20 ## Breaking changes * Remove `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.UserTokenRpcService` and `org.sonarsource.sonarlint.core.rpc.impl.SonarLintRpcServerImpl.getUserTokenService`. They were not useful anymore. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams` deprecated constructor. Use the other one. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService#checkSmartNotificationsSupported` method and associated parameter and response. They were not useful anymore. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto` deprecated constructor. Use the other one. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto` deprecated constructor. Use the other one. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsParams` deprecated constructor. Use the other one. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarCloudAlternativeEnvironmentDto` deprecated constructors. Use the other one. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto` deprecated constructor and the `getPid` getter. They were not useful anymore. * Remove the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.GetOrganizationParams` deprecated constructor. Use the other one. ## New features * Add a new constructor to `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` constructor to provide flags as an enum values list. * Add `analysisReportingTriggered` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService` * It is used to report usage of analysis features such as the analysis of VCS changed files, all project files or for the pre-commit analysis * Should not be implemented unless necessary ## Deprecation * Deprecate `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.FeatureFlagsDto` class. # 10.19 ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFilesAndTrackParams` constructor, and replace it with a new one without the `startTime` parameter, that is no longer relevant. # 10.17 ## New features * Add a new constructor to `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarCloudAlternativeEnvironmentDto` to accept a map of `SonarCloudRegion` to `SonarQubeCloudRegionDto`. * Per region a `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarQubeCloudRegionDto` must be provided that contains the *base*, *API* and *WebSocket* URIs. * `null` values are accepted for every URI - it will internally fallback to the actual region URIs for a `null` value encountered. ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService.checkSmartNotificationsSupported` method. It always returns that notifications are supported. # 10.16 ## New features * Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService` class, containing a `suggestFix(SuggestFixParams)` method. * Introduce a new `isAiCodeFixable` method in `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto`. ## Deprecation * Deprecate the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarCloudAlternativeEnvironmentDto` constructor. It is replaced by an overload in which the new API base URL should be provided. # 10.14 ## Breaking changes * Add new method `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#invalidToken` to notify client that WebAPI calls to SQS/SQC fails due to wrong token * Client can implement this method to offer user to change credentials for the connection to fix the problem * For now notification is being sent only for 401 Unauthorized HTTP response code since it's corresponds to malformed/wrong token and ignores 403 Forbidden response code since it's a user permissions problem that has to be addressed on the server * Also once notification sent, backend doesn't attempt to send any requests to server anymore until credentials changed * Add `region` to `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionParams` and `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionSuggestionDto` to support multi-region SQC connection configuration * Constructor without region parameter is removed * Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#didRaiseIssue` and associated types. See `raiseIssues` and `raiseHotspots` instead. * Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getIssueTrackingService` and associated types. Tracking is managed by the backend. * Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getSecurityHotspotMatchingService` and associated types. Tracking is managed by the backend. * Removed `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams#getServerUrl()`. Use `getConnectionParams` instead. * Removed `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService#analyzeFiles`. Use `analyzeFilesAndTrack` instead. * Removed deprecated methods in `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto`: * `getSeverity` * `getType` * `getCleanCodeAttribute` * `getImpacts` * Use `getSeverityMode` instead. * Removed deprecated methods in `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto`: * `getSeverity` * `getType` * `getCleanCodeAttribute` * `getImpacts` * Use `getSeverityMode` instead. ## New features * Add SonarCloud region parameter to * `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionParams` * `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto` * `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto` * `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.FuzzySearchUserOrganizationsParams` * `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.GetOrganizationParams` * `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.ListUserOrganizationsParams` * This is in order to support multi-region SQC connection configuration. Constructors without region parameter are deprecated * `org.sonarsource.sonarlint.core.commons.monitoring.MonitoringService#newTrace(String, String)` can be used internally to initialize a manual trace in Sentry * When monitoring is enabled, 1% of all analysis requests are sent to Sentry's performance tracing feature * Two new system properties can be used to tune the behavior of the Sentry integration: * `sonarlint.internal.monitoring.dsn` overrides the default [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/) (e.g. for tests) * `sonarlint.internal.monitoring.tracesSampleRate`, parsed as a `java.lang.Double`, overrides the default sampling rate of analysis requests # 10.13 ## Breaking changes * New feature flag `enableMonitoring` in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.FeatureFlagsDto` allows clients to opt into monitoring with Sentry ## New features * Introduce `org.sonarsource.sonarlint.core.rpc.protocol.backend.dogfooding.DogfoodingRpcService.isDogfoodingEnvironment` method to allow clients to know if it is running in a dogfooding environment * Will return `true` if `SONARSOURCE_DOGFOODING` environment variable is set and equals `"1"` * Will return `false` in all other cases * Introduce opt-in monitoring via Sentry * As a first step, the monitoring service is only initialized in dogfooding environments when the feature flag is set * All logging events sent to the client at the `ERROR` level are reported as monitoring events # 10.12 ## Breaking changes * Adapt `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams.languageSpecificRequirements to accept org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.JsTsRequirementsDto` instead of `clientNodeJsPath` ## New features * Introduce `bundlePath` initialization parameter in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.JsTsRequirementsDto` to allow clients to provide the path to the unzipped es-lint bridge bundle * The path will be passed down to the Js/Ts/CSS analyzer and will indicate that the analyzer does not need to unzip the bundle itself, thus reducing the usage of the `.sonarlint` temporary storage * Provide `null` to keep the previous behavior # 10.11 ## Breaking changes * Signature of `org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidUpdateFileSystemParams#DidUpdateFileSystemParams` was changed * Parameter `addedOrChangedFiles` was split into `addedFiles` and `changedFiles` * Removed parameter `branch` and `pullRequest` from `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto` as it should not be used anymore by the client. # 10.10 ## New features * Introduce a new method `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService.shouldUseEnterpriseCSharpAnalyzer` to allow clients to know what kind of C# analyzer should be used for the analysis. * The method returns a boolean value indicating whether the enterprise C# analyzer should be used or not * The method returns `true` if a binding exists for config scope AND the related connected server has the enterprise C# plugin (`csharpenterprise`) installed * The method returns `true` if binding exists with a SonarQube version < 10.8 (i.e. SQ versions that do not include repackaged dotnet analyzer) OR SonarCloud * The method returns `false` in standalone mode or if connected to non-commercial edition of SonarQube with a version >= 10.8 * Inject the relevant C# analyzer to analysis engines based on the above and share the path to the analyzer JAR as an analysis property for the OmniSharp plugin. ## Breaking changes * Add two new constructor arguments to `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.OmnisharpRequirementsDto` for clients to declare the paths to the Open-Source and Enterprise C# analyzers. # 10.9 ## New features * A new attribute `severityMode` has been added to `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto` that automatically contains either `StandardModeDetails` or `MQRModeDetails` * A new type `StandardModeDetails` has been introduced, which contains information about severity and type * A new type `MQRModeDetails` has been introduced, which contains information about clean code attribute and impacts * You should display the finding accordingly to the information contained by `severityMode` * A new method `IssueRpcService#getEffectiveIssueDetails` has been added to the backend to allow clients to retrieve detailed information about an issue * The method accepts a configuration scope ID and an issue ID (UUID) as parameters * The method returns a `GetEffectiveIssueDetailsResponse` object containing the detailed information about the issue * It is preferred to use this method instead of the `RulesRpcService#getEffectiveRuleDetails` when retrieving rule description details in the context of a specific issue, as this new method will provide more precise information based on the issue, like issue impacts & customized issue severity ## Breaking changes * Remove the `org.sonarsource.sonarlint.core.serverconnection.ServerPathProvider` class. * Remove `severity` and `type` fields from `org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto` as this class is only used for fetching standalone rule details, which should always have the Clean Code Attribute and Impacts ## Deprecation * The following attributes have been deprecated from `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto`, you should now use the new attribute `severityMode` * `severity` * `type` * `cleanCodeAttribute` * `impacts` # 10.7.1 ## Breaking changes * Add a new method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#matchProjectBranch` allowing the backend to check whether the locally checked-out branch matches a requesting server branch # 10.7 ## Breaking changes * Signature of `org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate#raiseHotspots` was changed * Parameter `issuesByFileUri` has been rightfully replaced by `hotspotsByFileUri` * This is purely a naming change, there is no functional impact ## New features * Add return value `GetForcedNodeJsResponse` to `org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate#didChangeClientNodeJsPath` indicating whether the Node.js path is effective or not. If that's the case, the path and the version will be returned. * It's not mandatory to use this return value. It is used by some IDEs to show the current Node.js version used. * Add a new system property `sonarlint.debug.active.rules` to log active rules in verbose mode when triggering an analysis # 10.6 ## Breaking changes * Signature of `org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate#noBindingSuggestionFound` was changed * Replaced parameter with `org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams` * Former parameter `projectKey` can now be accessed by `params.getProjectKey()` * Removed deprecated constructors from `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` ## New features * Add a field to `org.sonarsource.sonarlint.core.rpc.protocol.common.NoBindingSuggestionFoundParams` indicating whether the suggestion where no binding was found by is SonarCloud or not, can be used to display a more precise notification in the IDE rather than a generic one * Add a signature to `SloopLauncher.start`, allowing clients to add custom JVM arguments to the start of the process # 10.4 ## Breaking changes * Add new `isUserDefined` parameter into `org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto` * User-defined files will be included in the analysis. Non-user-defined files such as generated or library files will be excluded from analysis when analysis is triggered by the backend. If the analysis was forced by the client, exclusions are not respected. * Introduce a new parameter in the constructor of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.FeatureFlagsDto`: `canOpenFixSuggestion`. * This flag lets clients completely disable the opening a fix suggestion in the IDE, which can be useful if the feature is not yet available in the client. * Introduce a new initialization parameter `TelemetryMigrationDto` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` * The parameter is nullable and should be used only by the SLVS to migrate its telemetry. All other clients should provide `null` as a value. ## New features * Add a method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` to allow the backend to request client-defined file exclusions from the client before every standalone analysis. * `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#getFileExclusions` to request file exclusions * Add a field to `org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto` to allow the backend to distinguish non-user-defined files to exclude from analysis * Add `showFixSuggestion` method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` * It's only available when the feature flag `canOpenFixSuggestion` is enabled * When using this method, you will receive a single fix suggestion for a specific issue that should be displayed to the user * The user should have the possibility to accept or decline the fix suggestion * The fix suggestion can be displayed at different locations in the file * Add `fixSuggestionResolved` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService` * You should use this method whenever a fix suggestion has been accepted or declined * If the fix has multiple changes (snippets), you should call the method once for each * The `indexSnippet` should be filled if possible, it corresponds to the snippet index in the list of changes * If you do not know if the fix was accepted or declined at the snippet level, you should call the method once for the whole fix ### File events * Add the `didOpenFile` and `didCloseFile` methods to `org.sonarsource.sonarlint.core.rpc.protocol.backend.file.FileRpcService`. * Clients are supposed to call these methods when a file is opened in the editor or closed. ### Analysis * Add a new constructor in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` to let clients provide if automatic analysis is enabled. * Add a new `didChangeAutomaticAnalysisSetting` method in `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService` * Clients are expected to call it whenever users change the "enable automatic analysis" setting. * Add new methods to `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService` to force analysis * `analyzeFullProject` forces analysis all files of the project that was provided to backend by method `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#listFiles` * `analyzeFileList` forces analysis for the provided set of files * `analyzeOpenFiles` forces analysis of all files that were reported as opened using `org.sonarsource.sonarlint.core.rpc.protocol.backend.file.FileRpcService#didOpenFile` * `analyzeVCSChangedFiles` forces analysis of modified and not committed files # 10.3.2 ## Breaking changes * Change `disabledLanguagesForAnalysis` parameter of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` introduced in 10.3 version to `disabledPluginKeysForAnalysis` * Analysis will be disabled for plugins specified in `disabledPluginKeysForAnalysis` but it will be still possible to consume Rule Descriptions * Can be null or empty if clients do not wish to disable analysis for any loaded plugin # 10.3 ## Breaking changes * Add new `disabledLanguagesForAnalysis` parameter into `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` * Analysis will be disabled for languages specified `disabledLanguagesForAnalysis` but it will be still possible to consume Rule Descriptions * Can be null or empty if clients do not wish to disable analysis for any loaded plugin ## New features * Add a method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` to allow backend to request inferred analysis properties from the client before every analysis. It's important because properties may change depending on files being analysed. * `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#getInferredAnalysisProperties` to request inferred properties * Add a method to the `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService` to let the client notify the backend with user defined analysis properties * `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService#didSetUserAnalysisProperties` to set user defined properties * For analysis, both user-defined and inferred properties will be merged. If the same property is inferred by the client and provided by the user - the inferred value will be used for analysis. ### Open Issue in IDE * Add the `getConnectionParams` method to `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams` * It allows clients to get parameters to create either SonarQube or SonarCloud connection * This field type is `Either` * Common methods of both connection types are added to the `AssistCreatingConnectionParams` class to provide users simplicity ## Deprecation * Deprecate `isSonarCloud` parameter from `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams` * This value on no longer needed on the backend side. * `org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams.getServerUrl` is only meaningful for SQ connections. Use `getConnection().getLeft().getServerUrl()` instead to get the `serverUrl` of a SQ connection * The existing constructor in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams` is now deprecated, the newly added constructor should be used instead (see above). # 10.2 ## Breaking changes * `org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate#didDetectSecret` had no `configScopeId` parameter, it was added ## New features ### Analysis and tracking * Add the `analyzeFilesAndTrack` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService`. * It allows clients to submit files for analysis, let the backend deal with issue tracking, and will lead to a later notification via `raiseIssues` and `raiseHotspots` (see below). * Usages of `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService#analyzeFiles` should be replaced by this new method. * It accepts a `AnalyzeFilesAndTrackParams` object instead of the deprecated `AnalyzeFilesParams`. The extra flag `shouldFetchServerIssues` should be set to `true` when the analysis is triggered in response to a file open event. * When using this method, implementation of the `didRaiseIssue` method of `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` is no longer required. The new `raiseIssues` and `raiseHotspots` methods should be implemented instead (see below). * Add `raiseIssues` method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` to report tracked issues. * Will be called by the backend when issues should be raised to the user. The UI should be updated accordingly. * The `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaiseIssuesParams` class contains a list of issues to raise by file URI. * Each raised issue went through issue tracking, and has potentially been matched with a previously known issue and/or a server issue in connected mode. * This new method reports a collection of issues replacing the ones previously raised. Every call contains the full list of known issues. * Add `raiseHotspots` method to `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient` to report tracked Security Hotspots. * Will be called by the backend when hotspots should be raised to the user. The UI should be updated accordingly. * The `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaiseHotspotsParams` class contains a list of hotspots to raise by file URI. * Each raised hotspot went through hotspot tracking, and has potentially been matched with a previously known hotspot and/or a server hotspot in connected mode. * This new method reports a collection of hotspots replacing the ones previously raised. Every call contains the full list of known hotspots. * Add `getRawIssues` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFilesResponse` * It allows clients to get raised issues in the analysis response. * This method is temporarily added and will be removed when the deprecated APIs have been dropped. ## Deprecation * `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService#analyzeFiles` and the underlying DTOs are deprecated, should be replaced by `analyzeFilesAndTrack`. * As a consequence, `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.didRaiseIssue` and the underlying DTOs are now deprecated. It should be replaced by `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.raiseIssues` and `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient.raiseHotspots`. * `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.IssueTrackingRpcService` and the underlying DTOs are deprecated, the functionality is now handled by `analyzeFilesAndTrack`. * `org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.SecurityHotspotMatchingRpcService` and the underlying DTOs are deprecated, the functionality is now handled by `analyzeFilesAndTrack`. * `org.sonarsource.sonarlint.core.client.legacy.analysis.SonarLintAnalysisEngine` and all classes from the `sonarlint-java-client-legacy` module are now deprecated. Analysis should happen via `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService#analyzeFilesAndTrack`, and the `sonarlint-java-client-legacy` artifact will soon be removed. * The `pid` parameter of the `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto` constructor is not used anymore (the backend PID is used instead). The constructor is now deprecated, and a new constructor without this parameter was introduced and should be used. The `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto.getPid` method is not used anymore and also deprecated. # 10.1 ## Breaking changes * Replace the last constructor parameter of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams`. * Clients should provide an instance of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.LanguageSpecificRequirements`. * The previous Node.js path parameter is now part of this new `LanguageSpecificRequirements`, together with configuration related to Omnisharp. * For clients not executing analysis via the backend, or not supporting C#, a `null` value can be passes as the 2nd parameter of the `LanguageSpecificRequirements` constructor * Introduce a new parameter in the constructor of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto`. * Clients should provide the PID of the host process. * For clients not executing analysis via the backend, this parameter is not used, so a dummy value can be provided. * Introduce a new parameter in the constructor of `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.FeatureFlagsDto`: `enableTelemetry`. * This flag lets clients completely disable the telemetry, which can be useful when using Sloop in the context of tests. * The flag replaces the `sonarlint.telemetry.disabled` system property. * For clients that want to keep the same behavior, they can read the system property on the client side and pass it to the `FeatureFlagsDto` constructor. * Stop leaking LSP4J types in API (SLCORE-663) and wrap them in SonarLint classes instead * `org.eclipse.lsp4j.jsonrpc.messages.Either` replaced by `org.sonarsource.sonarlint.core.rpc.protocol.common.Either` * `org.eclipse.lsp4j.jsonrpc.CancelChecker` replaced by `org.sonarsource.sonarlint.core.rpc.client.SonarLintCancelChecker` * Add new client method `org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate#suggestConnection`. * This method is used when binding settings are found for an unknown connection. * Clients are expected to notify users about these. * Move class `org.sonarsource.sonarlint.core.rpc.protocol.backend.usertoken.RevokeTokenParams` to `org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.RevokeTokenParams`. * Introduce a new parameter in the constructor of `org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto`. * Clients can provide a detected language for the file. This is the opportunity to rely on the IDE's detected type. * This is used for analysis, clients can pass `null` to keep the same behavior as before, or if no language was detected. ## New features ### Connected mode: sharing setup * Add methods to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService` to track binding creation. * `addedManualBindings` should be called by clients when a new binding is created manually. * `addedImportedBindings` should be called by clients when a binding is created from a shared connected mode configuration file. * `addedAutomaticBindings` should be called by clients when a binding is created using a suggestion from a server analysis settings files. * Add `isFromSharedConfiguration` field to `org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams` and `org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto`. * This field tells the client whether the binding suggestion comes from a shared connected mode configuration file (`true`) or from analysis settings files (`false`). ### Analysis * Add the `analyzeFiles` method in `org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService`. * Clients can use this method to trigger an analysis. It's a request, so they can get a response with details about the analysis. * They need to pass the list of URIs representing files to analyze. * They also need to pass an "analysis ID", which is a unique ID used for correlating the analysis and issues that are raised via `didRaiseIssue` (see below). * Add the `didRaiseIssue` method in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * This lets clients be informed when an issue is detected during analysis. * They can do local tracking or stream the issue to the users. * They can retrieve which analysis lead to this issue being raised with the "analysis ID" correlation ID. * Add the `didSkipLoadingPlugin` method in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * This is called after an analysis when a plugin was not loaded. * Clients are expected to notify users about these. * Add the `didDetectSecret` method in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * This is called after an analysis when a secret was detected in one of the analyzed files. * Clients are expected to notify users about these. * The backend does not keep track of any notification regarding secrets detection. Clients will need to manage some cache to avoid notifying users too often. * Add the `promoteExtraEnabledLanguagesInConnectedMode` method in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * This is called after an analysis in standalone mode when a language enabled only in connected mode was detected. * Clients are expected to notify users about these. * The backend does not keep track of any notification regarding this promotion. Clients will need to manage some cache to avoid notifying users too often. * Add the `getBaseDir` method in `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient`. * This is called during an analysis to determine the base directory for the files being analyzed. * Clients are expected to implement this request if they support analysis via the backend. ================================================ FILE: LICENSE.txt ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: NOTICE.txt ================================================ SonarLint Core Copyright (C) SonarSource Sàrl mailto:info AT sonarsource DOT com This product includes software developed at SonarSource (http://www.sonarsource.com/). ================================================ FILE: README.md ================================================ SonarLint Core ============== The core library to run SonarQube for IDE analysis (used by SonarQube for IDE Eclipse, IntelliJ and VS), as well as the SonarQube for IDE language server (used by SonarQube for IDE VSCode) with the goal of helping developers deliver [integrated code quality and security](https://www.sonarsource.com/solutions/for-developers/). [![Build Status](https://github.com/SonarSource/sonarlint-core/actions/workflows/build.yml/badge.svg)](https://github.com/SonarSource/sonarlint-core/actions) [![Quality Gate Status](https://next.sonarqube.com/sonarqube/api/project_badges/measure?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&metric=alert_status)](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent) Have Questions or Feedback? --------------------------- For SonarQube for IDE support questions ("How do I?", "I got this error, why?", ...), please first read the [FAQ](https://community.sonarsource.com/t/frequently-asked-questions/7204) and then head to the [SonarSource forum](https://community.sonarsource.com/c/help/sl). There are chances that a question similar to yours has already been answered. Be aware that this forum is a community, so the standard pleasantries ("Hi", "Thanks", ...) are expected. And if you don't get an answer to your thread, you should sit on your hands for at least three days before bumping it. Operators are not standing by. :-) Contributing ------------ If you would like to see a new feature, please create a new thread in the forum ["Suggest new features"](https://community.sonarsource.com/c/suggestions/features). Please be aware that we are not actively looking for feature contributions. The truth is that it's extremely difficult for someone outside SonarSource to comply with our roadmap and expectations. Therefore, we typically only accept minor cosmetic changes and typo fixes. With that in mind, if you would like to submit a code contribution, please create a pull request for this repository. Please explain your motives to contribute this change: what problem you are trying to fix, what improvement you are trying to make. Make sure that you follow our [code style](https://github.com/SonarSource/sonar-developer-toolset#code-style) and all tests are passing (Travis build is executed for each pull request). Building -------- To build sources locally follow these instructions. ### Build and Run Unit Tests #### Prerequisites Some medium tests load plugins relying on Node.js, so make sure the latest LTS version is installed and `node` is in the PATH. Execute from the project base directory: mvn verify ### Run integration tests See [Running Integration Tests](its/README.md) Documentation ------------- Have a look at the documentation [here](spec/README.adoc). License ------- Copyright SonarSource. Licensed under the [GNU Lesser General Public License, Version 3.0](http://www.gnu.org/licenses/lgpl.txt) ================================================ FILE: SECURITY.md ================================================ # Reporting Security Issues A mature software vulnerability treatment process is a cornerstone of a robust information security management system. Contributions from the community play an important role in the evolution and security of our products, and in safeguarding the security and privacy of our users. If you believe you have discovered a security vulnerability in Sonar's products, we encourage you to report it immediately. To responsibly report a security issue, please email us at [security@sonarsource.com](mailto:security@sonarsource.com). Sonar’s security team will acknowledge your report, guide you through the next steps, or request additional information if necessary. Customers with a support contract can also report the vulnerability directly through the support channel. For security vulnerabilities found in third-party libraries, please also contact the library's owner or maintainer directly. ## Responsible Disclosure Policy For more information about disclosing a security vulnerability to Sonar, please refer to our community post: [Responsible Vulnerability Disclosure](https://community.sonarsource.com/t/responsible-vulnerability-disclosure/9317). ================================================ FILE: backend/analysis-engine/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-analysis-engine SonarLint Core - Analysis Engine Run analysis com.google.code.findbugs jsr305 provided ${project.groupId} sonarlint-plugin-commons ${project.version} ${project.groupId} sonarlint-commons ${project.version} commons-codec commons-codec org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test org.mockito mockito-junit-jupiter test org.awaitility awaitility test ch.qos.logback logback-classic test org.apache.maven.plugins maven-dependency-plugin copy-open-source-plugins-for-mediumtests generate-test-resources copy org.sonarsource.python sonar-python-plugin 5.18.0.31561 jar ${project.build.directory}/plugins false true false conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisQueue.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.PriorityQueue; import java.util.function.Predicate; import java.util.stream.Stream; import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand; import org.sonarsource.sonarlint.core.analysis.command.Command; import org.sonarsource.sonarlint.core.analysis.command.NotifyModuleEventCommand; import org.sonarsource.sonarlint.core.analysis.command.ResetPluginsCommand; import org.sonarsource.sonarlint.core.analysis.command.UnregisterModuleCommand; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static java.util.Map.entry; public class AnalysisQueue { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String ANALYSIS_EXPIRATION_DELAY_PROPERTY_NAME = "sonarqube.ide.internal.analysis.expiration.delay"; private static final Duration ANALYSIS_EXPIRATION_DEFAULT_DELAY = Duration.ofMinutes(1); private final Duration analysisExpirationDelay = getAnalysisExpirationDelay(); private final PriorityQueue queue = new PriorityQueue<>(new CommandComparator()); public synchronized void post(Command command) { queue.add(new QueuedCommand(command)); LOG.debug("Posting command in analysis queue: {}, new size is {}", command, queue.size()); notifyAll(); } public synchronized void wakeUp() { notifyAll(); } public synchronized List removeAll() { var pendingTasks = new ArrayList<>(queue); queue.clear(); return pendingTasks.stream().map(QueuedCommand::getCommand).toList(); } public synchronized Command takeNextCommand() throws InterruptedException { while (true) { var firstReadyCommand = pollNextReadyCommand(); if (firstReadyCommand.isPresent()) { var queuedCommand = firstReadyCommand.get(); LOG.debug("Picked command from the queue: {}, {} remaining", queuedCommand.command, queue.size()); return tidyUp(queuedCommand); } // wait for a new command to come in wait(); } } public synchronized void clearAllButAnalysesAndResets() { removeAll(queuedCommand -> !(queuedCommand.command instanceof AnalyzeCommand) && !(queuedCommand.command instanceof ResetPluginsCommand)); } private Optional pollNextReadyCommand() { var commandsToKeep = new ArrayList(); // cannot use iterator as priority order is not guaranteed while (!queue.isEmpty()) { var candidateCommand = queue.poll(); if (candidateCommand.command.shouldCancelQueue()) { candidateCommand.command.cancel(); LOG.debug("Not picking next command {}, is canceled", candidateCommand.command); } else { if (candidateCommand.command.isReady()) { queue.addAll(commandsToKeep); return Optional.of(candidateCommand); } LOG.debug("Not picking next command {}, is not ready", candidateCommand.command); commandsToKeep.add(candidateCommand); } } queue.addAll(commandsToKeep); return Optional.empty(); } private Command tidyUp(QueuedCommand nextCommand) { cleanUpExpiredCommands(nextCommand); return batchAutomaticAnalyses(nextCommand.command); } private void cleanUpExpiredCommands(QueuedCommand nextQueuedCommand) { var notReadyCommands = removeAll(queuedCommand -> !queuedCommand.command.isReady() && queuedCommand.getQueuedTime().plus(analysisExpirationDelay).isBefore(Instant.now())); if (!notReadyCommands.isEmpty()) { LOG.debug("Canceling {} not ready analyses", notReadyCommands.size()); } if (nextQueuedCommand.command instanceof UnregisterModuleCommand unregisterCommand) { var expiredCommands = removeAll( queuedCommand -> (queuedCommand.command instanceof AnalyzeCommand analyzeCommand && analyzeCommand.getModuleKey().equals(unregisterCommand.getModuleKey())) || queuedCommand.command instanceof NotifyModuleEventCommand); if (!expiredCommands.isEmpty()) { LOG.debug("Canceling {} analyses expired by module unregistration", expiredCommands.size()); } } } private Command batchAutomaticAnalyses(Command nextCommand) { if (nextCommand instanceof AnalyzeCommand analyzeCommand && analyzeCommand.getTriggerType().canBeBatchedWithSameTriggerType()) { var removedCommands = (List) removeAll(otherQueuedCommand -> canBeBatched(analyzeCommand, otherQueuedCommand.command)); if (removedCommands.isEmpty()) { return analyzeCommand; } LOG.debug("Batching {} analyses", removedCommands.size() + 1); return Stream.concat(Stream.of(analyzeCommand), removedCommands.stream()) .sorted((c1, c2) -> (int) (c1.getSequenceNumber() - c2.getSequenceNumber())) .reduce(AnalyzeCommand::mergeWith) // this last clause should never occur .orElse(analyzeCommand); } return nextCommand; } private static boolean canBeBatched(AnalyzeCommand analyzeCommand, Command otherCommand) { return otherCommand instanceof AnalyzeCommand otherAnalyzeCommand && otherAnalyzeCommand.getModuleKey().equals(analyzeCommand.getModuleKey()) && otherAnalyzeCommand.getTriggerType().canBeBatchedWithSameTriggerType(); } private List removeAll(Predicate predicate) { var iterator = queue.iterator(); var removedCommands = new ArrayList(); while (iterator.hasNext()) { var queuedCommand = iterator.next(); if (predicate.test(queuedCommand)) { iterator.remove(); queuedCommand.command.cancel(); removedCommands.add(queuedCommand.command); } } return removedCommands; } private static class QueuedCommand { private final Command command; private final Instant queuedTime = Instant.now(); QueuedCommand(Command command) { this.command = command; } public Command getCommand() { return command; } public Instant getQueuedTime() { return queuedTime; } } private static class CommandComparator implements Comparator { private static final Map, Integer> COMMAND_TYPES_ORDERED = Map.ofEntries( // reset commands should be pulled first from the queue, they cancel unregister and file event commands and make use of more up-to-date plugins for subsequent analyses entry(ResetPluginsCommand.class, 0), // then unregister commands might make file events and analyses irrelevant entry(UnregisterModuleCommand.class, 1), // then forwarding file events takes priority over analyses, to make sure they give more accurate results entry(NotifyModuleEventCommand.class, 2), // then analyses have the lowest priority entry(AnalyzeCommand.class, 3)); @Override public int compare(QueuedCommand queuedCommand, QueuedCommand otherQueuedCommand) { var command = queuedCommand.command; var otherCommand = otherQueuedCommand.command; var commandRank = COMMAND_TYPES_ORDERED.get(command.getClass()); var otherCommandRank = COMMAND_TYPES_ORDERED.get(otherCommand.getClass()); return !Objects.equals(commandRank, otherCommandRank) ? (commandRank - otherCommandRank) : // for same command types, respect insertion order (int) (command.getSequenceNumber() - otherCommand.getSequenceNumber()); } } private static Duration getAnalysisExpirationDelay() { try { var analysisExpirationDelayFromSystemProperty = System.getProperty(ANALYSIS_EXPIRATION_DELAY_PROPERTY_NAME); var parsedDelay = Duration.parse(analysisExpirationDelayFromSystemProperty); LOG.debug("Overriding analysis expiration delay with value from system property: {}", parsedDelay); return parsedDelay; } catch (RuntimeException e) { LOG.debug("Using default analysis expiration delay: {}", ANALYSIS_EXPIRATION_DEFAULT_DELAY); return ANALYSIS_EXPIRATION_DEFAULT_DELAY; } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisScheduler.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.analysis.command.Command; import org.sonarsource.sonarlint.core.analysis.command.ResetPluginsCommand; import org.sonarsource.sonarlint.core.analysis.container.global.GlobalAnalysisContainer; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; public class AnalysisScheduler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Runnable CANCELING_TERMINATION = () -> { }; private final AtomicReference globalAnalysisContainer = new AtomicReference<>(); private final AnalysisQueue analysisQueue = new AnalysisQueue(); private final Thread analysisThread = new Thread(this::executeQueuedCommands, "sonarlint-analysis-scheduler"); private final LogOutput logOutput; private final AtomicReference termination = new AtomicReference<>(); private final AtomicReference executingCommand = new AtomicReference<>(); public AnalysisScheduler(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins, @Nullable LogOutput logOutput) { this.logOutput = logOutput; // if the container cannot be started, the thread won't be started var analysisContainer = new GlobalAnalysisContainer(analysisGlobalConfig, loadedPlugins); analysisContainer.startComponents(); globalAnalysisContainer.set(analysisContainer); analysisThread.start(); } public void reset(Supplier pluginsWithConfigSupplier) { post(new ResetPluginsCommand(globalAnalysisContainer, analysisQueue, pluginsWithConfigSupplier)); } public void wakeUp() { analysisQueue.wakeUp(); } private void executeQueuedCommands() { while (termination.get() == null) { SonarLintLogger.get().setTarget(logOutput); try { var command = analysisQueue.takeNextCommand(); executingCommand.set(command); if (termination.get() == CANCELING_TERMINATION) { break; } executingCommand.get().execute(globalAnalysisContainer.get().getModuleRegistry()); executingCommand.set(null); } catch (InterruptedException e) { if (termination.get() != CANCELING_TERMINATION) { LOG.error("Analysis engine interrupted", e); } } catch (Exception e) { LOG.debug("Analysis command failed", e); } } termination.get().run(); } public void post(Command command) { LOG.debug("Post: " + Thread.currentThread().getName() + " " + Thread.currentThread().threadId()); LOG.debug("Posting command from Scheduler: " + command); if (termination.get() != null) { LOG.error("Analysis engine stopping, ignoring command"); command.cancel(); return; } if (!analysisThread.isAlive()) { LOG.error("Analysis engine not started, ignoring command"); command.cancel(); return; } var currentCommand = executingCommand.get(); if (currentCommand != null && command.shouldCancelPost(currentCommand)) { LOG.debug("Cancelling queuing of command"); currentCommand.cancel(); } LOG.debug("Posting command from Scheduler to queue: " + command); analysisQueue.post(command); } public void stop() { if (!analysisThread.isAlive()) { return; } if (!termination.compareAndSet(null, CANCELING_TERMINATION)) { // already terminating return; } var command = executingCommand.getAndSet(null); if (command != null) { command.cancel(); } analysisThread.interrupt(); analysisQueue.removeAll().forEach(Command::cancel); globalAnalysisContainer.get().stopComponents(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/SchedulerResetConfiguration.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; /** * Pairs an {@link AnalysisSchedulerConfiguration} with the {@link LoadedPlugins} it was built * alongside, so both are always resolved atomically inside a * {@link org.sonarsource.sonarlint.core.analysis.command.ResetPluginsCommand}. */ public record SchedulerResetConfiguration(AnalysisSchedulerConfiguration config, LoadedPlugins plugins) { } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisConfiguration.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.concurrent.Immutable; import org.sonar.api.batch.rule.ActiveRule; @Immutable public class AnalysisConfiguration { private final List inputFiles; private final Map extraProperties; private final Path baseDir; private final Collection activeRules; private final String toString; private AnalysisConfiguration(Builder builder) { this.baseDir = builder.baseDir; this.inputFiles = builder.inputFiles; this.extraProperties = builder.extraProperties; this.activeRules = builder.activeRules; this.toString = generateToString(); } public static Builder builder() { return new Builder(); } public Map extraProperties() { return extraProperties; } public Path baseDir() { return baseDir; } public List inputFiles() { return inputFiles; } public Collection activeRules() { return activeRules; } @Override public String toString() { return toString; } private String generateToString() { var sb = new StringBuilder(); sb.append("[\n"); generateToStringCommon(sb); generateToStringActiveRules(sb); generateToStringInputFiles(sb); sb.append("]\n"); return sb.toString(); } protected void generateToStringActiveRules(StringBuilder sb) { if ("true".equals(System.getProperty("sonarlint.debug.active.rules"))) { sb.append(" activeRules: ").append(activeRules).append("\n"); } else { // Group active rules by language and count occurrences var languageCounts = new HashMap(); for (var rule : activeRules) { var languageKey = rule.ruleKey().toString().split(":")[0]; languageCounts.put(languageKey, languageCounts.getOrDefault(languageKey, 0) + 1); } sb.append(" activeRules: ["); languageCounts.forEach((language, count) -> sb.append(count).append(" ").append(language).append(", ")); if (!languageCounts.isEmpty()) { // Remove the trailing comma and space sb.setLength(sb.length() - 2); } sb.append("]\n"); } } protected void generateToStringCommon(StringBuilder sb) { sb.append(" baseDir: ").append(baseDir()).append("\n"); sb.append(" extraProperties: ").append(extraProperties()).append("\n"); } protected void generateToStringInputFiles(StringBuilder sb) { sb.append(" inputFiles: [\n"); for (ClientInputFile inputFile : inputFiles()) { sb.append(" ").append(inputFile.uri()); sb.append(" (").append(getCharsetLabel(inputFile)).append(")"); if (inputFile.isTest()) { sb.append(" [test]"); } var language = inputFile.language(); if (language != null) { sb.append(" [").append(language.getSonarLanguageKey()).append("]"); } sb.append("\n"); } sb.append(" ]\n"); } private static String getCharsetLabel(ClientInputFile inputFile) { var charset = inputFile.getCharset(); return charset != null ? charset.displayName() : "default"; } public static final class Builder { private final List inputFiles = new ArrayList<>(); private final Map extraProperties = new HashMap<>(); private Path baseDir; private final Collection activeRules = new ArrayList<>(); private Builder() { } public Builder addInputFiles(ClientInputFile... inputFiles) { Collections.addAll(this.inputFiles, inputFiles); return this; } public Builder addInputFiles(Collection inputFiles) { this.inputFiles.addAll(inputFiles); return this; } public Builder addInputFile(ClientInputFile inputFile) { this.inputFiles.add(inputFile); return this; } public Builder putAllExtraProperties(Map extraProperties) { extraProperties.forEach(this::putExtraProperty); return this; } public Builder putExtraProperty(String key, String value) { this.extraProperties.put(key.trim(), value); return this; } public Builder setBaseDir(Path baseDir) { this.baseDir = baseDir; return this; } public Builder addActiveRules(ActiveRule... activeRules) { Collections.addAll(this.activeRules, activeRules); return this; } public Builder addActiveRules(Collection activeRules) { this.activeRules.addAll(activeRules); return this; } public Builder addActiveRule(ActiveRule activeRules) { this.activeRules.add(activeRules); return this; } public AnalysisConfiguration build() { return new AnalysisConfiguration(this); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisResults.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.time.Duration; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class AnalysisResults { private final Set failedAnalysisFiles = new LinkedHashSet<>(); private final Map languagePerFile = new LinkedHashMap<>(); private Duration duration = Duration.ZERO; public void addFailedAnalysisFile(ClientInputFile inputFile) { failedAnalysisFiles.add(inputFile); } /** * Detected languages for each file. * The values in the map can be null if no language was detected for some files. */ public Map languagePerFile() { return languagePerFile; } public void setLanguageForFile(ClientInputFile file, @Nullable SonarLanguage language) { this.languagePerFile.put(file, language); } /** * Input files for which there were analysis errors. The analyzers failed to correctly handle these files, and therefore there might be issues * missing or no issues at all for these files. */ public Collection failedAnalysisFiles() { return failedAnalysisFiles; } public Duration getDuration() { return duration; } public void setDuration(Duration duration) { this.duration = duration; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisSchedulerConfiguration.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @Immutable public class AnalysisSchedulerConfiguration { private static final String NODE_EXECUTABLE_PROPERTY = "sonar.nodejs.executable"; private final Path workDir; private final Map extraProperties; private final Path nodeJsPath; private final long clientPid; private final Function fileSystemProvider; private AnalysisSchedulerConfiguration(Builder builder) { this.workDir = builder.workDir; this.extraProperties = new LinkedHashMap<>(builder.extraProperties); this.nodeJsPath = builder.nodeJsPath; this.clientPid = builder.clientPid; this.fileSystemProvider = builder.fileSystemProvider; } public static Builder builder() { return new Builder(); } public Path getWorkDir() { return workDir; } public long getClientPid() { return clientPid; } public Function getFileSystemProvider() { return fileSystemProvider; } public Map getEffectiveSettings() { Map props = new HashMap<>(extraProperties); if (nodeJsPath != null) { props.put(NODE_EXECUTABLE_PROPERTY, nodeJsPath.toString()); } return props; } public static final class Builder { private Path workDir; private Map extraProperties = Collections.emptyMap(); private Path nodeJsPath; private long clientPid; private Function fileSystemProvider = key -> null; private Builder() { } /** * Override default work dir (~/.sonarlint/work) */ public Builder setWorkDir(Path workDir) { this.workDir = workDir; return this; } /** * Properties that will be passed to global extensions */ public Builder setExtraProperties(Map extraProperties) { this.extraProperties = extraProperties; return this; } /** * Set the location of the nodejs executable used by some analyzers. */ public Builder setNodeJs(@Nullable Path nodeJsPath) { this.nodeJsPath = nodeJsPath; return this; } public Builder setClientPid(long clientPid) { this.clientPid = clientPid; return this; } public Builder setFileSystemProvider(Function fileSystemProvider) { this.fileSystemProvider = fileSystemProvider; return this; } public AnalysisSchedulerConfiguration build() { return new AnalysisSchedulerConfiguration(this); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFile.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; /** * InputFile as provided by client * @since 1.1 */ public interface ClientInputFile { /** * The absolute file path. It needs to correspond to a file in the filesystem because some plugins don't use {@link #contents} * or {@link inputStream} yet, and will attempt to access the file directly. * @deprecated avoid calling this method if possible, since it may require to create a temporary copy of the file */ @Deprecated String getPath(); /** * Flag an input file as test file. Analyzers may apply different rules on test files. */ boolean isTest(); /** * Charset to be used to read file content. If null it means the charset is unknown and analysis will likely use JVM default encoding to read the file. */ @CheckForNull Charset getCharset(); /** * Language key of the file. If not null, language detection based on the file name suffix is skipped. The file will be analyzed by an analyzer that can * handle the language. */ @CheckForNull default SonarLanguage language() { return null; } /** * Allow clients to pass their own object to ease mapping back to IDE file. */ G getClientObject(); /** * Gets a stream of the contents of the file. */ InputStream inputStream() throws IOException; /** * Gets the contents of the file. */ String contents() throws IOException; /** * Logical relative path with '/' separators. Used to apply SonarLintPathPatterns and by some analyzers. Example: 'src/main/java/Foo.java'. * Can be project relative path when it makes sense. */ String relativePath(); /** * URI to uniquely identify this file. */ URI uri(); default boolean isDirty() { return false; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileEdit.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.util.List; public class ClientInputFileEdit { private final ClientInputFile target; private final List textEdits; public ClientInputFileEdit(ClientInputFile target, List textEdits) { this.target = target; this.textEdits = textEdits; } public ClientInputFile target() { return target; } public List textEdits() { return textEdits; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/ClientModuleFileEvent.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent; public class ClientModuleFileEvent { private final ClientInputFile target; private final ModuleFileEvent.Type type; private ClientModuleFileEvent(ClientInputFile target, ModuleFileEvent.Type type) { this.target = target; this.type = type; } public static ClientModuleFileEvent of(ClientInputFile target, ModuleFileEvent.Type type) { return new ClientModuleFileEvent(target, type); } public ClientInputFile target() { return target; } public ModuleFileEvent.Type type() { return type; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/ClientModuleFileSystem.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.util.stream.Stream; import org.sonar.api.batch.fs.InputFile; public interface ClientModuleFileSystem { Stream files(String suffix, InputFile.Type type); Stream files(); } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/ClientModuleInfo.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; public class ClientModuleInfo { private final String key; private final ClientModuleFileSystem clientFileSystem; public ClientModuleInfo(String key, ClientModuleFileSystem clientFileSystem) { this.key = key; this.clientFileSystem = clientFileSystem; } public String key() { return key; } public ClientModuleFileSystem fileSystem() { return clientFileSystem; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/DefaultLocation.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.batch.fs.TextRange; public class DefaultLocation implements IssueLocation { private final String message; private final ClientInputFile inputFile; private final org.sonarsource.sonarlint.core.commons.api.TextRange textRange; public DefaultLocation(@Nullable ClientInputFile inputFile, @Nullable TextRange textRange, @Nullable String message) { this.textRange = textRange != null ? WithTextRange.convert(textRange) : null; this.inputFile = inputFile; this.message = message; } @Override public ClientInputFile getInputFile() { return inputFile; } @Override public String getMessage() { return message; } @CheckForNull @Override public org.sonarsource.sonarlint.core.commons.api.TextRange getTextRange() { return textRange; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/Flow.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.util.List; import java.util.stream.Collectors; import org.sonar.api.batch.sensor.issue.IssueLocation; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; public class Flow { private final List locations; public Flow(List issueLocations) { this.locations = issueLocations.stream() .map(i -> new DefaultLocation( i.inputComponent().isFile() ? ((SonarLintInputFile) i.inputComponent()).getClientInputFile() : null, i.textRange(), i.message())) .collect(Collectors.toList()); } public List locations() { return locations; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/Issue.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.TextRange; public class Issue implements IssueLocation { private final String primaryMessage; private final ClientInputFile clientInputFile; private final List flows; private final List quickFixes; private final Optional ruleDescriptionContextKey; private final TextRange textRange; private final ActiveRule activeRule; private final Map overriddenImpacts; public Issue(ActiveRule activeRule, @Nullable String primaryMessage, Map overriddenImpacts, @Nullable org.sonar.api.batch.fs.TextRange textRange, @Nullable ClientInputFile clientInputFile, List flows, List quickFixes, Optional ruleDescriptionContextKey) { this.activeRule = activeRule; this.overriddenImpacts = overriddenImpacts; this.textRange = Optional.ofNullable(textRange).map(WithTextRange::convert).orElse(null); this.primaryMessage = primaryMessage; this.clientInputFile = clientInputFile; this.flows = flows; this.quickFixes = quickFixes; this.ruleDescriptionContextKey = ruleDescriptionContextKey; } public ActiveRule getActiveRule() { return activeRule; } public RuleKey getRuleKey() { return activeRule.ruleKey(); } @Override public String getMessage() { return primaryMessage; } @Override @CheckForNull public ClientInputFile getInputFile() { return clientInputFile; } public List flows() { return flows; } public List quickFixes() { return quickFixes; } @Override @CheckForNull public TextRange getTextRange() { return textRange; } public Map getOverriddenImpacts() { return overriddenImpacts; } public Optional getRuleDescriptionContextKey() { return ruleDescriptionContextKey; } @Override public String toString() { var sb = new StringBuilder(); sb.append("["); sb.append("rule=").append(getRuleKey()); if (textRange != null) { var startLine = textRange.getStartLine(); sb.append(", line=").append(startLine); } if (clientInputFile != null) { sb.append(", file=").append(clientInputFile.uri()); } sb.append("]"); return sb.toString(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/IssueLocation.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import javax.annotation.CheckForNull; public interface IssueLocation extends WithTextRange { @CheckForNull String getMessage(); /** * @return null for global issues */ @CheckForNull ClientInputFile getInputFile(); } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/QuickFix.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.util.List; public class QuickFix { private final List inputFileEdits; private final String message; public QuickFix(List inputFileEdits, String message) { this.inputFileEdits = inputFileEdits; this.message = message; } public List inputFileEdits() { return inputFileEdits; } public String message() { return message; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/TextEdit.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import org.sonarsource.sonarlint.core.commons.api.TextRange; public class TextEdit { private final TextRange range; private final String newText; public TextEdit(TextRange range, String newText) { this.range = range; this.newText = newText; } public TextRange range() { return range; } public String newText() { return newText; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/TriggerType.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; public enum TriggerType { AUTO(true), FORCED(false); private final boolean canBeBatchedWithSameTriggerType; TriggerType(boolean canBeBatchedWithSameTriggerType) { this.canBeBatchedWithSameTriggerType = canBeBatchedWithSameTriggerType; } public boolean canBeBatchedWithSameTriggerType() { return canBeBatchedWithSameTriggerType; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/WithTextRange.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.api.TextRange; public interface WithTextRange { /** * @return null for file level issues */ @CheckForNull TextRange getTextRange(); @CheckForNull default Integer getStartLine() { var textRange = getTextRange(); return textRange != null ? textRange.getStartLine() : null; } @CheckForNull default Integer getStartLineOffset() { var textRange = getTextRange(); return textRange != null ? textRange.getStartLineOffset() : null; } @CheckForNull default Integer getEndLine() { var textRange = getTextRange(); return textRange != null ? textRange.getEndLine() : null; } @CheckForNull default Integer getEndLineOffset() { var textRange = getTextRange(); return textRange != null ? textRange.getEndLineOffset() : null; } static TextRange convert(org.sonar.api.batch.fs.TextRange analyzerTextRange) { return new TextRange( analyzerTextRange.start().line(), analyzerTextRange.start().lineOffset(), analyzerTextRange.end().line(), analyzerTextRange.end().lineOffset()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/AnalyzeCommand.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import static org.sonarsource.sonarlint.core.commons.util.StringUtils.pluralize; public class AnalyzeCommand extends Command { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final String moduleKey; private final UUID analysisId; private final TriggerType triggerType; private final Supplier configurationSupplier; private final Consumer issueListener; @Nullable private final Trace trace; private final CompletableFuture futureResult; private final SonarLintCancelMonitor cancelMonitor; private final TaskManager taskManager; private final Consumer> analysisStarted; private final Supplier isReadySupplier; private final Set files; private final Map extraProperties; public AnalyzeCommand(String moduleKey, UUID analysisId, TriggerType triggerType, Supplier configurationSupplier, Consumer issueListener, @Nullable Trace trace, SonarLintCancelMonitor cancelMonitor, TaskManager taskManager, Consumer> analysisStarted, Supplier isReadySupplier, Set files, Map extraProperties) { this(moduleKey, analysisId, triggerType, configurationSupplier, issueListener, trace, cancelMonitor, taskManager, analysisStarted, isReadySupplier, files, extraProperties, new CompletableFuture<>()); } public AnalyzeCommand(String moduleKey, UUID analysisId, TriggerType triggerType, Supplier configurationSupplier, Consumer issueListener, @Nullable Trace trace, SonarLintCancelMonitor cancelMonitor, TaskManager taskManager, Consumer> analysisStarted, Supplier isReadySupplier, Set files, Map extraProperties, CompletableFuture futureResult) { this.moduleKey = moduleKey; this.analysisId = analysisId; this.triggerType = triggerType; this.configurationSupplier = configurationSupplier; this.issueListener = issueListener; this.trace = trace; this.cancelMonitor = cancelMonitor; this.taskManager = taskManager; this.analysisStarted = analysisStarted; this.isReadySupplier = isReadySupplier; this.files = files; this.extraProperties = extraProperties; this.futureResult = futureResult; taskManager.trackNewTask(analysisId, cancelMonitor); } @Override public boolean isReady() { return isReadySupplier.get(); } public String getModuleKey() { return moduleKey; } public TriggerType getTriggerType() { return triggerType; } public CompletableFuture getFutureResult() { return futureResult; } public Set getFiles() { return files; } public Map getExtraProperties() { return extraProperties; } @Override public void execute(ModuleRegistry moduleRegistry) { try { var configuration = configurationSupplier.get(); taskManager.runExistingTask(moduleKey, analysisId, "Analyzing " + pluralize(configuration.inputFiles().size(), "file"), null, true, true, indicator -> execute(moduleRegistry, indicator, configuration), cancelMonitor); } catch (Exception e) { handleAnalysisFailed(e); } } void execute(ModuleRegistry moduleRegistry, ProgressIndicator progressIndicator, AnalysisConfiguration configuration) { try { doExecute(moduleRegistry, progressIndicator, configuration); } catch (Exception e) { handleAnalysisFailed(e); } } void doExecute(ModuleRegistry moduleRegistry, ProgressIndicator progressIndicator, AnalysisConfiguration analysisConfig) { try { LOG.info("Starting analysis with configuration: {}", analysisConfig); var analysisResults = doRunAnalysis(moduleRegistry, progressIndicator, analysisConfig); futureResult.complete(analysisResults); } catch (CompletionException e) { handleAnalysisFailed(e.getCause()); } catch (Exception e) { handleAnalysisFailed(e); } } private void handleAnalysisFailed(Throwable throwable) { LOG.error("Error during analysis", throwable); futureResult.completeExceptionally(throwable); } private AnalysisResults doRunAnalysis(ModuleRegistry moduleRegistry, ProgressIndicator progressIndicator, AnalysisConfiguration configuration) { var startTime = System.currentTimeMillis(); analysisStarted.accept(configuration.inputFiles()); if (configuration.inputFiles().isEmpty()) { LOG.info("No file to analyze"); futureResult.complete(new AnalysisResults()); return new AnalysisResults(); } var moduleContainer = moduleRegistry.getContainerFor(moduleKey); Throwable originalException = null; doIfTraceIsSet(t -> { int filesCount = configuration.inputFiles().size(); t.setData("filesCount", filesCount); var fileSizes = new ArrayList<>(filesCount); var languages = new HashSet<>(); configuration.inputFiles().forEach(f -> { try { fileSizes.add(f.contents().length()); } catch (IOException | IllegalStateException e) { fileSizes.add(0); } Optional.ofNullable(f.language()) .map(SonarLanguage::getSonarLanguageKey) .ifPresent(languages::add); }); t.setData("fileSizes", fileSizes); t.setData("languages", languages); t.setData("activeRulesCount", configuration.activeRules().size()); }); try { var issueCounter = new AtomicInteger(0); Consumer issueCountingListener = issue -> { issueCounter.incrementAndGet(); issueListener.accept(issue); }; var result = moduleContainer.analyze(configuration, issueCountingListener, progressIndicator, trace); doIfTraceIsSet(t -> { t.setData("failedFilesCount", result.failedAnalysisFiles().size()); t.setData("foundIssuesCount", issueCounter.get()); t.finishSuccessfully(); }); result.setDuration(Duration.ofMillis(System.currentTimeMillis() - startTime)); return result; } catch (Throwable e) { originalException = e; doIfTraceIsSet(t -> t.finishExceptionally(e)); throw e; } finally { try { if (moduleContainer.isTransient()) { moduleContainer.stopComponents(); } } catch (Exception e) { if (originalException != null) { e.addSuppressed(originalException); } throw e; } } } private void doIfTraceIsSet(Consumer action) { if (trace != null) { action.accept(trace); } } public AnalyzeCommand mergeWith(AnalyzeCommand otherNewerAnalyzeCommand) { var analysisConfiguration = configurationSupplier.get(); var newerAnalysisConfiguration = otherNewerAnalyzeCommand.configurationSupplier.get(); var mergedInputFiles = new ArrayList<>(newerAnalysisConfiguration.inputFiles()); var newInputFileUris = newerAnalysisConfiguration.inputFiles().stream().map(ClientInputFile::uri).collect(Collectors.toSet()); for (ClientInputFile inputFile : analysisConfiguration.inputFiles()) { if (!newInputFileUris.contains(inputFile.uri())) { mergedInputFiles.add(inputFile); } } var mergedAnalysisConfiguration = AnalysisConfiguration.builder() .addActiveRules(newerAnalysisConfiguration.activeRules()) .setBaseDir(newerAnalysisConfiguration.baseDir()) .putAllExtraProperties(newerAnalysisConfiguration.extraProperties()) .addInputFiles(mergedInputFiles) .build(); return new AnalyzeCommand(moduleKey, analysisId, triggerType, () -> mergedAnalysisConfiguration, issueListener, trace, new SonarLintCancelMonitor(), taskManager, analysisStarted, isReadySupplier, mergedInputFiles.stream().map(ClientInputFile::uri).collect(Collectors.toSet()), newerAnalysisConfiguration.extraProperties(), futureResult); } @Override public void cancel() { if (!cancelMonitor.isCanceled()) { cancelMonitor.cancel(); } if (!futureResult.isCancelled()) { futureResult.cancel(true); } } @Override public boolean shouldCancelPost(Command executingCommand) { if (!(executingCommand instanceof AnalyzeCommand analyzeCommand)) { return false; } if (cancelMonitor.isCanceled() || futureResult.isCancelled()) { return true; } var triggerTypesMatch = getTriggerType() == analyzeCommand.getTriggerType(); var filesMatch = Objects.equals(getFiles(), analyzeCommand.getFiles()); var extraPropertiesMatch = Objects.equals(getExtraProperties(), analyzeCommand.getExtraProperties()); return triggerTypesMatch && filesMatch && extraPropertiesMatch; } @Override public boolean shouldCancelQueue() { return cancelMonitor.isCanceled() || futureResult.isCancelled(); } @CheckForNull public Trace getTrace() { return trace; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/Command.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import java.util.concurrent.atomic.AtomicLong; import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry; public abstract class Command { private static final AtomicLong analysisGlobalNumber = new AtomicLong(); private final long sequenceNumber = analysisGlobalNumber.incrementAndGet(); public abstract void execute(ModuleRegistry moduleRegistry); public final long getSequenceNumber() { return sequenceNumber; } public boolean isReady() { return true; } public void cancel() { // most commands are not cancelable } public boolean shouldCancelPost(Command executingCommand) { return false; } public boolean shouldCancelQueue() { return false; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/NotifyModuleEventCommand.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileEvent; import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry; import org.sonarsource.sonarlint.core.analysis.container.module.ModuleFileEventNotifier; public class NotifyModuleEventCommand extends Command { private final String moduleKey; private final ClientModuleFileEvent event; public NotifyModuleEventCommand(String moduleKey, ClientModuleFileEvent event) { this.moduleKey = moduleKey; this.event = event; } @Override public void execute(ModuleRegistry moduleRegistry) { var moduleContainer = moduleRegistry.getContainerIfStarted(moduleKey); if (moduleContainer != null) { moduleContainer.getComponentByType(ModuleFileEventNotifier.class).fireModuleFileEvent(event); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/ResetPluginsCommand.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.sonarsource.sonarlint.core.analysis.AnalysisQueue; import org.sonarsource.sonarlint.core.analysis.SchedulerResetConfiguration; import org.sonarsource.sonarlint.core.analysis.container.global.GlobalAnalysisContainer; import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry; public class ResetPluginsCommand extends Command { private final Supplier schedulerResetConfigurationSupplier; private final AtomicReference globalAnalysisContainer; private final AnalysisQueue analysisQueue; public ResetPluginsCommand(AtomicReference globalAnalysisContainer, AnalysisQueue analysisQueue, Supplier schedulerResetConfigurationSupplier) { this.schedulerResetConfigurationSupplier = schedulerResetConfigurationSupplier; this.globalAnalysisContainer = globalAnalysisContainer; this.analysisQueue = analysisQueue; } @Override public void execute(ModuleRegistry moduleRegistry) { globalAnalysisContainer.get().stopComponents(); var pluginsWithConfig = schedulerResetConfigurationSupplier.get(); globalAnalysisContainer.set(new GlobalAnalysisContainer(pluginsWithConfig.config(), pluginsWithConfig.plugins())); globalAnalysisContainer.get().startComponents(); analysisQueue.clearAllButAnalysesAndResets(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/UnregisterModuleCommand.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry; public final class UnregisterModuleCommand extends Command { private final String moduleKey; public UnregisterModuleCommand(String moduleKey) { this.moduleKey = moduleKey; } @Override public void execute(ModuleRegistry moduleRegistry) { moduleRegistry.unregisterModule(moduleKey); } public String getModuleKey() { return moduleKey; } @Override public boolean shouldCancelPost(Command executingCommand) { return executingCommand instanceof AnalyzeCommand analyzeCommand && analyzeCommand.getModuleKey().equals(moduleKey); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/command/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.command; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/ContainerLifespan.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container; public enum ContainerLifespan { INSTANCE, MODULE, ANALYSIS } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisConfigurationProvider.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.ConfigurationBridge; import org.springframework.context.annotation.Bean; public class AnalysisConfigurationProvider { private Configuration analysisConfig; @Bean("configuration") public Configuration provide(AnalysisSettings settings) { if (analysisConfig == null) { this.analysisConfig = new ConfigurationBridge(settings); } return analysisConfig; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisContainer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.resources.Languages; import org.sonar.api.scan.filesystem.PathResolver; import org.sonarsource.sonarlint.core.analysis.container.ContainerLifespan; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileIndexer; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.InputFileBuilder; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.InputFileIndex; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.LanguageDetection; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintFileSystem; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.IssueFilters; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.EnforceIssuesFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.IgnoreIssuesFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.SonarLintNoSonarFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueExclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueInclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader; import org.sonarsource.sonarlint.core.analysis.container.analysis.sensor.SensorOptimizer; import org.sonarsource.sonarlint.core.analysis.container.analysis.sensor.SensorsExecutor; import org.sonarsource.sonarlint.core.analysis.container.analysis.sensor.SonarLintSensorStorage; import org.sonarsource.sonarlint.core.analysis.container.global.AnalysisExtensionInstaller; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorContext; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpFileLinesContextFactory; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; public class AnalysisContainer extends SpringComponentContainer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ProgressIndicator cancelMonitor; public AnalysisContainer(SpringComponentContainer globalContainer, ProgressIndicator progressIndicator) { super(globalContainer); this.cancelMonitor = progressIndicator; } @Override protected void doBeforeStart() { addCoreComponents(); addPluginExtensions(); } private void addCoreComponents() { add( cancelMonitor, SonarLintInputProject.class, NoOpFileLinesContextFactory.class, // temp new AnalysisTempFolderProvider(), // file system PathResolver.class, // lang Languages.class, AnalysisSettings.class, new AnalysisConfigurationProvider(), // file system InputFileIndex.class, InputFileBuilder.class, FileMetadata.class, LanguageDetection.class, FileIndexer.class, SonarLintFileSystem.class, // Exclusions using SonarQube properties EnforceIssuesFilter.class, IgnoreIssuesFilter.class, IssueExclusionPatternInitializer.class, IssueInclusionPatternInitializer.class, IssueExclusionsLoader.class, SensorOptimizer.class, SensorsExecutor.class, DefaultSensorContext.class, SonarLintSensorStorage.class, IssueFilters.class, // rules CheckFactory.class, // issues SonarLintNoSonarFilter.class); } private void addPluginExtensions() { getParent().getComponentByType(AnalysisExtensionInstaller.class).install(this, ContainerLifespan.ANALYSIS); } @Override protected void doAfterStart() { LOG.debug("Start analysis"); // Don't initialize Sensors before the FS is indexed getComponentByType(FileIndexer.class).index(); getComponentByType(SensorsExecutor.class).execute(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisSettings.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import java.util.HashMap; import java.util.Map; import org.sonar.api.config.PropertyDefinitions; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.container.global.GlobalSettings; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; public class AnalysisSettings extends MapSettings { public AnalysisSettings(GlobalSettings globalSettings, AnalysisConfiguration analysisConfig, PropertyDefinitions propertyDefinitions) { super(propertyDefinitions, mergeInOrder(globalSettings, analysisConfig)); } private static Map mergeInOrder(GlobalSettings globalSettings, AnalysisConfiguration analysisConfig) { Map result = new HashMap<>(globalSettings.getProperties()); result.putAll(analysisConfig.extraProperties()); return result; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisTempFolderProvider.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import java.io.File; import javax.annotation.Nullable; import org.sonar.api.utils.TempFolder; import org.springframework.context.annotation.Bean; public class AnalysisTempFolderProvider { private final NoTempFilesDuringAnalysis instance = new NoTempFilesDuringAnalysis(); @Bean("tempFolder") public TempFolder provide() { return instance; } private static class NoTempFilesDuringAnalysis implements TempFolder { @Override public File newDir() { throw throwUOEFolder(); } @Override public File newDir(String name) { throw throwUOEFolder(); } private static UnsupportedOperationException throwUOEFolder() { return new UnsupportedOperationException("Don't create temp folders during analysis"); } @Override public File newFile() { throw throwUOEFiles(); } private static UnsupportedOperationException throwUOEFiles() { return new UnsupportedOperationException("Don't create temp files during analysis"); } @Override public File newFile(@Nullable String prefix, @Nullable String suffix) { throw throwUOEFiles(); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/IssueListenerHolder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import java.util.function.Consumer; import org.sonarsource.sonarlint.core.analysis.api.Issue; /** * We need a dedicated class for dependency injection * */ public class IssueListenerHolder { private final Consumer wrapped; public IssueListenerHolder(Consumer issueListener) { this.wrapped = issueListener; } public void handle(Issue issue) { wrapped.accept(issue); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/SonarLintPathPattern.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import javax.annotation.Nullable; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.utils.PathUtils; import org.sonar.api.utils.WildcardPattern; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Path relative to module basedir */ public class SonarLintPathPattern { private static final SonarLintLogger LOG = SonarLintLogger.get(); final WildcardPattern pattern; public SonarLintPathPattern(String pattern) { if (pattern.startsWith("file:")) { LOG.warn("Unsupported path pattern: " + pattern); pattern = pattern.replaceAll("^file:/*", ""); } if (!pattern.startsWith("**/")) { pattern = "**/" + pattern; } this.pattern = WildcardPattern.create(pattern); } public static SonarLintPathPattern[] create(String[] s) { var result = new SonarLintPathPattern[s.length]; for (var i = 0; i < s.length; i++) { result[i] = new SonarLintPathPattern(s[i]); } return result; } public boolean match(InputFile inputFile) { return match(inputFile.relativePath(), true); } public boolean match(String filePath) { return match(filePath, true); } public boolean match(InputFile inputFile, boolean caseSensitiveFileExtension) { return match(inputFile.relativePath(), caseSensitiveFileExtension); } public boolean match(String filePath, boolean caseSensitiveFileExtension) { var path = PathUtils.sanitize(filePath); if (!caseSensitiveFileExtension) { var extension = sanitizeExtension(FilenameUtils.getExtension(path)); if (StringUtils.isNotBlank(extension)) { path = Strings.CI.removeEnd(path, extension); path = path + extension; } } return path != null && pattern.match(path); } @Override public String toString() { return pattern.toString(); } static String sanitizeExtension(@Nullable String suffix) { return StringUtils.lowerCase(Strings.CS.removeStart(suffix, ".")); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/AbstractFilePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.stream.StreamSupport; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem.Index; import org.sonar.api.batch.fs.InputFile; /** * Partial implementation of {@link FilePredicate}. * @since 5.1 */ public abstract class AbstractFilePredicate implements OptimizedFilePredicate { protected static final int DEFAULT_PRIORITY = 10; @Override public Iterable filter(Iterable target) { return StreamSupport.stream(target.spliterator(), false).filter(AbstractFilePredicate.this::apply).toList(); } @Override public Iterable get(Index index) { return filter(index.inputFiles()); } @Override public int priority() { return DEFAULT_PRIORITY; } @Override public final int compareTo(OptimizedFilePredicate o) { return o.priority() - priority(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/AndPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem.Index; import org.sonar.api.batch.fs.InputFile; /** * @since 4.2 */ class AndPredicate extends AbstractFilePredicate { private final List predicates = new ArrayList<>(); private AndPredicate() { } public static FilePredicate create(Collection predicates) { if (predicates.isEmpty()) { return TruePredicate.TRUE; } var result = new AndPredicate(); for (FilePredicate filePredicate : predicates) { if (filePredicate == TruePredicate.TRUE) { continue; } else if (filePredicate == FalsePredicate.FALSE) { return FalsePredicate.FALSE; } else if (filePredicate instanceof AndPredicate andPredicate) { result.predicates.addAll(andPredicate.predicates); } else { result.predicates.add(OptimizedFilePredicateAdapter.create(filePredicate)); } } Collections.sort(result.predicates); return result; } @Override public boolean apply(InputFile f) { for (OptimizedFilePredicate predicate : predicates) { if (!predicate.apply(f)) { return false; } } return true; } @Override public Iterable filter(Iterable target) { var result = target; for (OptimizedFilePredicate predicate : predicates) { result = predicate.filter(result); } return result; } @Override public Iterable get(Index index) { if (predicates.isEmpty()) { return index.inputFiles(); } // Optimization, use get on first predicate then filter with next predicates var result = predicates.get(0).get(index); for (var i = 1; i < predicates.size(); i++) { result = predicates.get(i).filter(result); } return result; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/DefaultFilePredicates.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FilePredicates; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Status; import org.sonarsource.sonarlint.core.analysis.container.analysis.SonarLintPathPattern; /** * Factory of {@link org.sonar.api.batch.fs.FilePredicate} * * @since 4.2 */ public class DefaultFilePredicates implements FilePredicates { /** * Returns a predicate that always evaluates to true */ @Override public FilePredicate all() { return TruePredicate.TRUE; } /** * Returns a predicate that always evaluates to false */ @Override public FilePredicate none() { return FalsePredicate.FALSE; } @Override public FilePredicate hasAbsolutePath(String s) { throw new UnsupportedOperationException("hasAbsolutePath"); } /** * non-normalized path and Windows-style path are supported */ @Override public FilePredicate hasRelativePath(String s) { throw new UnsupportedOperationException("hasRelativePath"); } @Override public FilePredicate hasURI(URI uri) { return new URIPredicate(uri); } @Override public FilePredicate matchesPathPattern(String inclusionPattern) { return new PathPatternPredicate(new SonarLintPathPattern(inclusionPattern)); } @Override public FilePredicate matchesPathPatterns(String[] inclusionPatterns) { if (inclusionPatterns.length == 0) { return TruePredicate.TRUE; } var predicates = new FilePredicate[inclusionPatterns.length]; for (var i = 0; i < inclusionPatterns.length; i++) { predicates[i] = new PathPatternPredicate(new SonarLintPathPattern(inclusionPatterns[i])); } return or(predicates); } @Override public FilePredicate doesNotMatchPathPattern(String exclusionPattern) { return not(matchesPathPattern(exclusionPattern)); } @Override public FilePredicate doesNotMatchPathPatterns(String[] exclusionPatterns) { if (exclusionPatterns.length == 0) { return TruePredicate.TRUE; } return not(matchesPathPatterns(exclusionPatterns)); } @Override public FilePredicate hasPath(String s) { throw new UnsupportedOperationException("hasPath"); } @Override public FilePredicate is(File ioFile) { // Needed for SonarCFamily return hasURI(ioFile.toURI()); } @Override public FilePredicate hasLanguage(String language) { return new LanguagePredicate(language); } @Override public FilePredicate hasLanguages(Collection languages) { List list = new ArrayList<>(); for (String language : languages) { list.add(hasLanguage(language)); } return or(list); } @Override public FilePredicate hasLanguages(String... languages) { List list = new ArrayList<>(); for (String language : languages) { list.add(hasLanguage(language)); } return or(list); } @Override public FilePredicate hasType(InputFile.Type type) { return new TypePredicate(type); } @Override public FilePredicate not(FilePredicate p) { return new NotPredicate(p); } @Override public FilePredicate or(Collection or) { return OrPredicate.create(or); } @Override public FilePredicate or(FilePredicate... or) { return OrPredicate.create(Arrays.asList(or)); } @Override public FilePredicate or(FilePredicate first, FilePredicate second) { return OrPredicate.create(Arrays.asList(first, second)); } @Override public FilePredicate and(Collection and) { return AndPredicate.create(and); } @Override public FilePredicate and(FilePredicate... and) { return AndPredicate.create(Arrays.asList(and)); } @Override public FilePredicate and(FilePredicate first, FilePredicate second) { return AndPredicate.create(Arrays.asList(first, second)); } @Override public FilePredicate hasFilename(String s) { return new FilenamePredicate(s); } @Override public FilePredicate hasExtension(String s) { return new FileExtensionPredicate(s); } @Override public FilePredicate hasStatus(Status status) { throw new UnsupportedOperationException("hasStatus"); } @Override public FilePredicate hasAnyStatus() { return new StatusPredicate(null); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/DefaultTextPointer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.TextPointer; public class DefaultTextPointer implements TextPointer { private final int line; private final int lineOffset; public DefaultTextPointer(int line, int lineOffset) { this.line = line; this.lineOffset = lineOffset; } @Override public int line() { return line; } @Override public int lineOffset() { return lineOffset; } @Override public String toString() { return "[line=" + line + ", lineOffset=" + lineOffset + "]"; } @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != this.getClass()) { return false; } var other = (DefaultTextPointer) obj; return other.line == this.line && other.lineOffset == this.lineOffset; } @Override public int hashCode() { return 37 * this.line + lineOffset; } @Override public int compareTo(TextPointer o) { if (this.line == o.line()) { return Integer.compare(this.lineOffset, o.lineOffset()); } return Integer.compare(this.line, o.line()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/DefaultTextRange.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.TextPointer; import org.sonar.api.batch.fs.TextRange; public class DefaultTextRange implements TextRange { private final TextPointer start; private final TextPointer end; public DefaultTextRange(TextPointer start, TextPointer end) { this.start = start; this.end = end; } @Override public TextPointer start() { return start; } @Override public TextPointer end() { return end; } @Override public boolean overlap(TextRange another) { // [A,B] and [C,D] // B > C && D > A return this.end.compareTo(another.start()) > 0 && another.end().compareTo(this.start) > 0; } @Override public String toString() { return "Range[from " + start + " to " + end + "]"; } @Override public boolean equals(Object obj) { if (obj == null || obj.getClass() != this.getClass()) { return false; } var other = (DefaultTextRange) obj; return start.equals(other.start) && end.equals(other.end); } @Override public int hashCode() { return start.hashCode() * 17 + end.hashCode(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/FalsePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.Collections; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem.Index; import org.sonar.api.batch.fs.InputFile; class FalsePredicate extends AbstractFilePredicate { static final FilePredicate FALSE = new FalsePredicate(); @Override public boolean apply(InputFile inputFile) { return false; } @Override public Iterable filter(Iterable target) { return Collections.emptyList(); } @Override public Iterable get(Index index) { return Collections.emptyList(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/FileExtensionPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.Locale; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; public class FileExtensionPredicate extends AbstractFilePredicate { private final String extension; public FileExtensionPredicate(String extension) { this.extension = lowercase(extension); } @Override public boolean apply(InputFile inputFile) { return extension.equals(getExtension(inputFile)); } @Override public Iterable get(FileSystem.Index index) { return index.getFilesByExtension(extension); } public static String getExtension(InputFile inputFile) { return getExtension(inputFile.filename()); } static String getExtension(String name) { var index = name.lastIndexOf('.'); if (index < 0) { return ""; } return lowercase(name.substring(index + 1)); } private static String lowercase(String extension) { return extension.toLowerCase(Locale.ENGLISH); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/FileIndexer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.net.URI; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFileFilter; import org.sonar.api.utils.MessageException; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Index input files into {@link InputFileIndex}. */ @SonarLintSide public class FileIndexer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final InputFileBuilder inputFileBuilder; private final AnalysisConfiguration analysisConfiguration; private final AnalysisResults analysisResult; private final List filters; private final IssueExclusionsLoader issueExclusionsLoader; private final InputFileIndex inputFileCache; private ProgressReport progressReport; public FileIndexer(InputFileIndex inputFileCache, InputFileBuilder inputFileBuilder, AnalysisConfiguration analysisConfiguration, AnalysisResults analysisResult, IssueExclusionsLoader issueExclusionsLoader, Optional> filters) { this.inputFileCache = inputFileCache; this.inputFileBuilder = inputFileBuilder; this.analysisConfiguration = analysisConfiguration; this.analysisResult = analysisResult; this.issueExclusionsLoader = issueExclusionsLoader; this.filters = filters.orElse(List.of()); } public void index() { progressReport = new ProgressReport("Report about progress of file indexation", TimeUnit.SECONDS.toMillis(10)); progressReport.start("Index files"); var progress = new Progress(); try { indexFiles(inputFileCache, progress, analysisConfiguration.inputFiles()); } catch (Exception e) { progressReport.stop(null); throw e; } var totalIndexed = progress.count(); progressReport.stop(totalIndexed + " " + pluralizeFiles(totalIndexed) + " indexed"); } private static String pluralizeFiles(int count) { return count == 1 ? "file" : "files"; } private void indexFiles(InputFileIndex inputFileCache, Progress progress, Iterable inputFiles) { for (ClientInputFile file : inputFiles) { indexFile(inputFileCache, progress, file); } } private void indexFile(InputFileIndex inputFileCache, Progress progress, ClientInputFile file) { var inputFile = inputFileBuilder.create(file); if (accept(inputFile)) { analysisResult.setLanguageForFile(file, inputFile.getLanguage()); indexFile(inputFileCache, progress, inputFile); issueExclusionsLoader.addMulticriteriaPatterns(inputFile); } } private void indexFile(final InputFileIndex inputFileCache, final Progress status, final SonarLintInputFile inputFile) { inputFileCache.doAdd(inputFile); status.markAsIndexed(inputFile); } private boolean accept(InputFile indexedFile) { // InputFileFilter extensions. Might trigger generation of metadata for (InputFileFilter filter : filters) { if (!filter.accept(indexedFile)) { LOG.debug("'{}' excluded by {}", indexedFile, filter.getClass().getName()); return false; } } return true; } private class Progress { private final Set indexed = new HashSet<>(); void markAsIndexed(SonarLintInputFile inputFile) { if (indexed.contains(inputFile.uri())) { throw MessageException.of("File " + inputFile + " can't be indexed twice."); } indexed.add(inputFile.uri()); var size = indexed.size(); progressReport.message(() -> size + " files indexed... (last one was " + inputFile.uri() + ")"); } int count() { return indexed.size(); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/FileMetadata.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Computes hash of files. Ends of Lines are ignored, so files with * same content but different EOL encoding have the same hash. */ @SonarLintSide public class FileMetadata { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final char LINE_FEED = '\n'; private static final char CARRIAGE_RETURN = '\r'; public abstract static class CharHandler { protected void handleAll(char c) { } protected void handleIgnoreEoL(char c) { } protected void newLine() { } protected void eof() { } } private static class LineCounter extends CharHandler { private int lines = 1; boolean alreadyLoggedInvalidCharacter = false; private final URI fileUri; private final Charset encoding; LineCounter(URI fileUri, Charset encoding) { this.fileUri = fileUri; this.encoding = encoding; } @Override protected void handleAll(char c) { if (!alreadyLoggedInvalidCharacter && c == '\ufffd') { LOG.warn("Invalid character encountered in file '{}' at line {} for encoding {}. Please fix file content or configure the encoding.", fileUri, lines, encoding); alreadyLoggedInvalidCharacter = true; } } @Override protected void newLine() { lines++; } public int lines() { return lines; } } private static class LineOffsetCounter extends CharHandler { private int currentOriginalOffset = 0; private final List originalLineOffsets = new ArrayList<>(); private int lastValidOffset = 0; public LineOffsetCounter() { originalLineOffsets.add(0); } @Override protected void handleAll(char c) { currentOriginalOffset++; } @Override protected void newLine() { originalLineOffsets.add(currentOriginalOffset); } @Override protected void eof() { lastValidOffset = currentOriginalOffset; } public List getOriginalLineOffsets() { return originalLineOffsets; } public int getLastValidOffset() { return lastValidOffset; } } /** * Compute hash of an inputStream ignoring line ends differences. * Maximum performance is needed. */ public Metadata readMetadata(InputStream stream, Charset encoding, URI fileUri, @Nullable CharHandler otherHandler) { var lineCounter = new LineCounter(fileUri, encoding); var lineOffsetCounter = new LineOffsetCounter(); try (Reader reader = new BufferedReader(new InputStreamReader(stream, encoding))) { CharHandler[] handlers; if (otherHandler != null) { handlers = new CharHandler[] {lineCounter, lineOffsetCounter, otherHandler}; } else { handlers = new CharHandler[] {lineCounter, lineOffsetCounter}; } read(reader, handlers); } catch (IOException e) { throw new IllegalStateException(String.format("Fail to read file '%s' with encoding '%s'", fileUri, encoding), e); } return new Metadata(lineCounter.lines(), lineOffsetCounter.getOriginalLineOffsets().stream().mapToInt(i -> i).toArray(), lineOffsetCounter.getLastValidOffset()); } private static void read(Reader reader, CharHandler... handlers) throws IOException { char c; var i = reader.read(); var afterCR = false; while (i != -1) { c = (char) i; if (afterCR) { for (CharHandler handler : handlers) { if (c == CARRIAGE_RETURN) { handler.newLine(); handler.handleAll(c); } else if (c == LINE_FEED) { handler.handleAll(c); handler.newLine(); } else { handler.newLine(); handler.handleIgnoreEoL(c); handler.handleAll(c); } } afterCR = c == CARRIAGE_RETURN; } else if (c == LINE_FEED) { for (CharHandler handler : handlers) { handler.handleAll(c); handler.newLine(); } } else if (c == CARRIAGE_RETURN) { afterCR = true; for (CharHandler handler : handlers) { handler.handleAll(c); } } else { for (CharHandler handler : handlers) { handler.handleIgnoreEoL(c); handler.handleAll(c); } } i = reader.read(); } for (CharHandler handler : handlers) { if (afterCR) { handler.newLine(); } handler.eof(); } } public static class Metadata { private final int lines; private final int[] originalLineOffsets; private final int lastValidOffset; public Metadata(int lines, int[] originalLineOffsets, int lastValidOffset) { this.lines = lines; this.originalLineOffsets = originalLineOffsets; this.lastValidOffset = lastValidOffset; } public int lines() { return lines; } public int[] originalLineOffsets() { return originalLineOffsets; } public int lastValidOffset() { return lastValidOffset; } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/FilenamePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; public class FilenamePredicate extends AbstractFilePredicate { private final String filename; public FilenamePredicate(String filename) { this.filename = filename; } @Override public boolean apply(InputFile inputFile) { return filename.equals(inputFile.filename()); } @Override public Iterable get(FileSystem.Index index) { return index.getFilesByName(filename); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/InputFileBuilder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import org.sonar.api.batch.fs.InputFile.Type; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class InputFileBuilder { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final LanguageDetection langDetection; private final FileMetadata fileMetadata; private final IssueExclusionsLoader exclusionsScanner; public InputFileBuilder(LanguageDetection langDetection, FileMetadata fileMetadata, IssueExclusionsLoader exclusionsScanner) { this.langDetection = langDetection; this.fileMetadata = fileMetadata; this.exclusionsScanner = exclusionsScanner; } SonarLintInputFile create(ClientInputFile inputFile) { var defaultInputFile = new SonarLintInputFile(inputFile, f -> { LOG.debug("Initializing metadata of file {}", f.uri()); var charset = f.charset(); InputStream stream; try { stream = f.inputStream(); } catch (IOException e) { throw new IllegalStateException("Failed to open a stream on file: " + f.uri(), e); } return fileMetadata.readMetadata(stream, charset != null ? charset : Charset.defaultCharset(), f.uri(), exclusionsScanner.createCharHandlerFor(f)); }); defaultInputFile.setType(inputFile.isTest() ? Type.TEST : Type.MAIN); var fileLanguage = inputFile.language(); if (fileLanguage != null) { LOG.debug("Language of file \"{}\" is set to \"{}\"", inputFile.uri(), fileLanguage); defaultInputFile.setLanguage(fileLanguage); } else { defaultInputFile.setLanguage(langDetection.language(defaultInputFile)); } return defaultInputFile; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/InputFileIndex.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.api.sonarlint.SonarLintSide; @SonarLintSide public class InputFileIndex implements FileSystem.Index { private final Set inputFiles = new LinkedHashSet<>(); private final Map> filesByNameIndex = new LinkedHashMap<>(); private final Map> filesByExtensionIndex = new LinkedHashMap<>(); private final SortedSet languages = new TreeSet<>(); @Override public Iterable inputFiles() { return inputFiles; } public void doAdd(InputFile inputFile) { if (inputFile.language() != null) { languages.add(inputFile.language()); } inputFiles.add(inputFile); filesByNameIndex.computeIfAbsent(inputFile.filename(), f -> new LinkedHashSet<>()).add(inputFile); filesByExtensionIndex.computeIfAbsent(FileExtensionPredicate.getExtension(inputFile), f -> new LinkedHashSet<>()).add(inputFile); } @Override public InputFile inputFile(String relativePath) { throw new UnsupportedOperationException("inputFile(String relativePath)"); } @Override public Iterable getFilesByName(String filename) { return filesByNameIndex.get(filename); } @Override public Iterable getFilesByExtension(String extension) { return filesByExtensionIndex.get(extension); } protected SortedSet languages() { return languages; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/Language.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.Arrays; import java.util.Collection; public final class Language { private final String key; private final String name; private final String[] fileSuffixes; public Language(String key, String name, String... fileSuffixes) { this.key = key; this.name = name; this.fileSuffixes = fileSuffixes; } /** * For example "java". */ public String key() { return key; } /** * For example "Java" */ public String name() { return name; } /** * For example ["jav", "java"]. */ public Collection fileSuffixes() { return Arrays.asList(fileSuffixes); } @Override public String toString() { return name; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/LanguageDetection.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.net.URI; import java.text.MessageFormat; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import javax.annotation.CheckForNull; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.config.Configuration; import org.sonar.api.utils.MessageException; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Detect language of a source file based on its suffix and configured patterns. */ public class LanguageDetection { private static final SonarLintLogger LOG = SonarLintLogger.get(); /** * Lower-case extension -> languages */ private final Map extensionsByLanguage = new LinkedHashMap<>(); public LanguageDetection(Configuration config) { for (SonarLanguage language : SonarLanguage.values()) { var extensions = config.get(language.getFileSuffixesPropKey()).isPresent() ? config.getStringArray(language.getFileSuffixesPropKey()) : language.getDefaultFileSuffixes(); for (var i = 0; i < extensions.length; i++) { var suffix = extensions[i]; extensions[i] = sanitizeExtension(suffix); } extensionsByLanguage.put(language, extensions); } } @CheckForNull public SonarLanguage language(InputFile inputFile) { return detectLanguage(inputFile.filename(), inputFile.uri()); } private SonarLanguage detectLanguage(String fileName, URI fileUri) { SonarLanguage detectedLanguage = null; for (Entry languagePatterns : extensionsByLanguage.entrySet()) { if (isCandidateForLanguage(fileName, languagePatterns.getValue())) { if (detectedLanguage == null) { detectedLanguage = languagePatterns.getKey(); } else { // Language was already forced by another pattern throw MessageException.of(MessageFormat.format("Language of file \"{0}\" can not be decided as the file extension matches both {1} and {2}", fileUri, getDetails(detectedLanguage), getDetails(languagePatterns.getKey()))); } } } if (detectedLanguage != null) { LOG.debug("Language of file \"{}\" is detected to be \"{}\"", fileUri, detectedLanguage); return detectedLanguage; } return null; } private static boolean isCandidateForLanguage(String fileName, String[] extensions) { for (String extension : extensions) { if (fileName.toLowerCase(Locale.ENGLISH).endsWith("." + extension)) { return true; } } return false; } private String getDetails(SonarLanguage detectedLanguage) { return detectedLanguage + ": " + String.join(",", extensionsByLanguage.get(detectedLanguage)); } public static String sanitizeExtension(String suffix) { return StringUtils.lowerCase(Strings.CS.removeStart(suffix, ".")); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/LanguagePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.InputFile; /** * @since 4.2 */ class LanguagePredicate extends AbstractFilePredicate { private final String language; LanguagePredicate(String language) { this.language = language; } @Override public boolean apply(InputFile f) { return language.equals(f.language()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/NotPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.InputFile; /** * @since 4.2 */ class NotPredicate extends AbstractFilePredicate { private final FilePredicate predicate; NotPredicate(FilePredicate predicate) { this.predicate = predicate; } @Override public boolean apply(InputFile f) { return !predicate.apply(f); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/OptimizedFilePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; /** * Optimized version of FilePredicate allowing to speed up query by looking at InputFile by index. */ public interface OptimizedFilePredicate extends FilePredicate, Comparable { /** * Filter provided files to keep only the ones that are valid for this predicate */ Iterable filter(Iterable inputFiles); /** * Get all files that are valid for this predicate. */ Iterable get(FileSystem.Index index); /** * For optimization. FilePredicates will be applied in priority order. For example when doing * p.and(p1, p2, p3) then p1, p2 and p3 will be applied according to their priority value. Higher priority value * are applied first. * Assign a high priority when the predicate will likely highly reduce the set of InputFiles to filter. Also * {@link RelativePathPredicate} and AbsolutePathPredicate have a high priority since they are using cache index. */ int priority(); } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/OptimizedFilePredicateAdapter.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.InputFile; class OptimizedFilePredicateAdapter extends AbstractFilePredicate { private final FilePredicate unoptimizedPredicate; private OptimizedFilePredicateAdapter(FilePredicate unoptimizedPredicate) { this.unoptimizedPredicate = unoptimizedPredicate; } @Override public boolean apply(InputFile inputFile) { return unoptimizedPredicate.apply(inputFile); } public static OptimizedFilePredicate create(FilePredicate predicate) { if (predicate instanceof OptimizedFilePredicate optimizedFilePredicate) { return optimizedFilePredicate; } else { return new OptimizedFilePredicateAdapter(predicate); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/OrPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.ArrayList; import java.util.Collection; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.InputFile; /** * @since 4.2 */ class OrPredicate extends AbstractFilePredicate { private final Collection predicates = new ArrayList<>(); private OrPredicate() { } public static FilePredicate create(Collection predicates) { if (predicates.isEmpty()) { return TruePredicate.TRUE; } var result = new OrPredicate(); for (FilePredicate filePredicate : predicates) { if (filePredicate == TruePredicate.TRUE) { return TruePredicate.TRUE; } else if (filePredicate == FalsePredicate.FALSE) { continue; } else if (filePredicate instanceof OrPredicate orPredicate) { result.predicates.addAll(orPredicate.predicates); } else { result.predicates.add(filePredicate); } } return result; } @Override public boolean apply(InputFile f) { for (FilePredicate predicate : predicates) { if (predicate.apply(f)) { return true; } } return false; } Collection predicates() { return predicates; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/PathPatternPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.SonarLintPathPattern; /** * @since 4.2 */ class PathPatternPredicate extends AbstractFilePredicate { private final SonarLintPathPattern pattern; PathPatternPredicate(SonarLintPathPattern pattern) { this.pattern = pattern; } @Override public boolean apply(InputFile f) { return pattern.match(f); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/ProgressReport.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.util.function.Supplier; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class ProgressReport implements Runnable { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final long period; private Supplier messageSupplier = () -> ""; private final Thread thread; private String stopMessage = null; private volatile boolean stop = false; public ProgressReport(String threadName, long period) { this.period = period; thread = new Thread(this, threadName); thread.setDaemon(true); } @Override public void run() { while (!stop) { try { Thread.sleep(period); log(messageSupplier.get()); } catch (InterruptedException e) { break; } } if (stopMessage != null) { log(stopMessage); } } public void start(String startMessage) { log(startMessage); thread.start(); } public void message(Supplier messageSupplier) { this.messageSupplier = messageSupplier; } public void stop(@Nullable String stopMessage) { this.stopMessage = stopMessage; this.stop = true; thread.interrupt(); try { thread.join(1000); } catch (InterruptedException e) { // Ignore } } private static void log(String message) { LOG.info(message); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintFileSystem.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.SortedSet; import java.util.stream.StreamSupport; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FilePredicates; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputDir; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class SonarLintFileSystem implements FileSystem { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final DefaultFilePredicates filePredicates; private final Path baseDir; private Charset encoding; private final InputFileIndex inputFileCache; public SonarLintFileSystem(AnalysisConfiguration analysisConfiguration, InputFileIndex inputFileCache) { this.inputFileCache = inputFileCache; this.baseDir = analysisConfiguration.baseDir(); this.filePredicates = new DefaultFilePredicates(); } @Override public File workDir() { LOG.warn("No workDir in SonarLint"); return baseDir(); } @Override public InputDir inputDir(File dir) { return new SonarLintInputDir(dir.toPath()); } @Override public FilePredicates predicates() { return filePredicates; } @Override public File baseDir() { return baseDir.toFile(); } private SonarLintFileSystem setEncoding(Charset c) { LOG.debug("Setting filesystem encoding: " + c); this.encoding = c; return this; } @Override public Charset encoding() { if (encoding == null) { setEncoding(StreamSupport.stream(inputFiles().spliterator(), false) .map(InputFile::charset) .findFirst() .orElse(Charset.defaultCharset())); } return encoding; } @Override public InputFile inputFile(FilePredicate predicate) { var files = inputFiles(predicate); var iterator = files.iterator(); if (!iterator.hasNext()) { return null; } var first = iterator.next(); if (!iterator.hasNext()) { return first; } var sb = new StringBuilder(); sb.append("expected one element but was: <" + first); for (var i = 0; i < 4 && iterator.hasNext(); i++) { sb.append(", " + iterator.next()); } if (iterator.hasNext()) { sb.append(", ..."); } sb.append('>'); throw new IllegalArgumentException(sb.toString()); } public Iterable inputFiles() { return inputFiles(filePredicates.all()); } @Override public Iterable inputFiles(FilePredicate predicate) { return OptimizedFilePredicateAdapter.create(predicate).get(inputFileCache); } @Override public boolean hasFiles(FilePredicate predicate) { return inputFiles(predicate).iterator().hasNext(); } @Override public Iterable files(FilePredicate predicate) { return () -> StreamSupport.stream(inputFiles(predicate).spliterator(), false) .map(InputFile::file) .iterator(); } @Override public SortedSet languages() { return inputFileCache.languages(); } @Override public File resolvePath(String path) { throw new UnsupportedOperationException("resolvePath"); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputDir.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.net.URI; import java.nio.file.Path; import org.sonar.api.batch.fs.InputDir; import org.sonar.api.utils.PathUtils; /** * This is a simple placeholder. Issues on directories will be reported as project level issues. */ public class SonarLintInputDir implements InputDir { private final Path path; public SonarLintInputDir(Path path) { this.path = path; } @Override public String relativePath() { return absolutePath(); } @Override public String absolutePath() { return PathUtils.sanitize(path().toString()); } @Override public File file() { return path().toFile(); } @Override public Path path() { return path; } @Override public String key() { return absolutePath(); } @Override public URI uri() { return path.toUri(); } @Override public boolean isFile() { return false; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof SonarLintInputDir dir)) { return false; } return path().equals(dir.path()); } @Override public int hashCode() { return path().hashCode(); } @Override public String toString() { return "[path=" + path() + "]"; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFile.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextPointer; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.utils.PathUtils; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.Metadata; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class SonarLintInputFile implements InputFile { private final ClientInputFile clientInputFile; private final String relativePath; private SonarLanguage language; private Type type; private Metadata metadata; private final Function metadataGenerator; private boolean ignoreAllIssues; private final Set noSonarLines = new HashSet<>(); private Collection ignoreIssuesOnlineRanges; public SonarLintInputFile(ClientInputFile clientInputFile, Function metadataGenerator) { this.clientInputFile = clientInputFile; this.metadataGenerator = metadataGenerator; this.relativePath = PathUtils.sanitize(clientInputFile.relativePath()); } public void checkMetadata() { if (metadata == null) { this.metadata = metadataGenerator.apply(this); } } public ClientInputFile getClientInputFile() { return clientInputFile; } @Override public String relativePath() { return relativePath; } public SonarLintInputFile setLanguage(@Nullable SonarLanguage language) { this.language = language; return this; } public SonarLintInputFile setType(Type type) { this.type = type; return this; } @CheckForNull @Override public String language() { return language != null ? language.getSonarLanguageKey() : null; } @CheckForNull public SonarLanguage getLanguage() { return language; } @Override public Type type() { return type; } /** * @deprecated avoid calling this method if possible, since it may require to create a temporary copy of the file */ @Deprecated(forRemoval = false) @Override public String absolutePath() { return PathUtils.sanitize(clientInputFile.getPath()); } /** * @deprecated avoid calling this method if possible, since it may require to create a temporary copy of the file */ @Deprecated @Override public File file() { return path().toFile(); } /** * @deprecated avoid calling this method if possible, since it may require to create a temporary copy of the file */ @Deprecated @Override public Path path() { return Paths.get(clientInputFile.getPath()); } @Override public InputStream inputStream() throws IOException { return clientInputFile.inputStream(); } @Override public String contents() throws IOException { return clientInputFile.contents(); } @Override public Status status() { return Status.ADDED; } /** * Component key. */ @Override public String key() { return uri().toString(); } @Override public URI uri() { return clientInputFile.uri(); } @Override public Charset charset() { var charset = clientInputFile.getCharset(); return charset != null ? charset : Charset.defaultCharset(); } @Override public String md5Hash() { try { return DigestUtils.md5Hex(contents()); } catch (IOException e) { throw new IllegalStateException("Unable to compute md5Hash for " + uri(), e); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof SonarLintInputFile file)) { return false; } return uri().equals(file.uri()); } @Override public int hashCode() { return uri().hashCode(); } @Override public String toString() { return "[uri=" + uri() + "]"; } @Override public boolean isFile() { return true; } @Override public String filename() { return Paths.get(relativePath).getFileName().toString(); } @Override public int lines() { checkMetadata(); return metadata.lines(); } @Override public boolean isEmpty() { checkMetadata(); return metadata.lastValidOffset() == 0; } @Override public TextPointer newPointer(int line, int lineOffset) { checkMetadata(); return new DefaultTextPointer(line, lineOffset); } @Override public TextRange newRange(TextPointer start, TextPointer end) { checkMetadata(); return newRangeValidPointers(start, end); } @Override public TextRange newRange(int startLine, int startLineOffset, int endLine, int endLineOffset) { checkMetadata(); var start = newPointer(startLine, startLineOffset); var end = newPointer(endLine, endLineOffset); return newRangeValidPointers(start, end); } @Override public TextRange selectLine(int line) { checkMetadata(); var startPointer = newPointer(line, 0); var endPointer = newPointer(line, lineLength(line)); return newRangeValidPointers(startPointer, endPointer); } private static TextRange newRangeValidPointers(TextPointer start, TextPointer end) { return new DefaultTextRange(start, end); } private int lineLength(int line) { return lastValidGlobalOffsetForLine(line) - metadata.originalLineOffsets()[line - 1]; } private int lastValidGlobalOffsetForLine(int line) { return line < this.metadata.lines() ? (metadata.originalLineOffsets()[line] - 1) : metadata.lastValidOffset(); } public void noSonarAt(Set noSonarLines) { this.noSonarLines.addAll(noSonarLines); } public boolean hasNoSonarAt(int line) { return this.noSonarLines.contains(line); } public boolean isIgnoreAllIssues() { checkMetadata(); return ignoreAllIssues; } public void setIgnoreAllIssues(boolean ignoreAllIssues) { this.ignoreAllIssues = ignoreAllIssues; } public void addIgnoreIssuesOnLineRanges(Collection lineRanges) { if (this.ignoreIssuesOnlineRanges == null) { this.ignoreIssuesOnlineRanges = new ArrayList<>(); } this.ignoreIssuesOnlineRanges.addAll(lineRanges); } public boolean isIgnoreAllIssuesOnLine(@Nullable Integer line) { checkMetadata(); if (line == null || ignoreIssuesOnlineRanges == null) { return false; } return ignoreIssuesOnlineRanges.stream().anyMatch(r -> r[0] <= line && line <= r[1]); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputProject.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.InputModule; import org.sonar.api.scanner.fs.InputProject; public class SonarLintInputProject implements InputModule, InputProject { public static final String SONARLINT_FAKE_PROJECT_KEY = "sonarlint"; @Override public String key() { return SONARLINT_FAKE_PROJECT_KEY; } @Override public boolean isFile() { return false; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/StatusPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; public class StatusPredicate extends AbstractFilePredicate { private final InputFile.Status status; StatusPredicate(@Nullable InputFile.Status status) { this.status = status; } @Override public boolean apply(InputFile f) { return status == null || status == f.status(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/TruePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FileSystem.Index; import org.sonar.api.batch.fs.InputFile; class TruePredicate extends AbstractFilePredicate { static final FilePredicate TRUE = new TruePredicate(); @Override public boolean apply(InputFile inputFile) { return true; } @Override public Iterable get(Index index) { return index.inputFiles(); } @Override public Iterable filter(Iterable target) { return target; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/TypePredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.sonar.api.batch.fs.InputFile; /** * @since 4.2 */ class TypePredicate extends AbstractFilePredicate { private final InputFile.Type type; TypePredicate(InputFile.Type type) { this.type = type; } @Override public boolean apply(InputFile f) { return type == f.type(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/URIPredicate.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.net.URI; import org.sonar.api.batch.fs.InputFile; class URIPredicate extends AbstractFilePredicate { private final URI uri; URIPredicate(URI uri) { this.uri = uri; } @Override public boolean apply(InputFile f) { return uri.equals(f.uri()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * This package is a part of bootstrap process, so we should take care about backward compatibility. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/DefaultIssueFilterChain.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import java.util.List; import org.sonar.api.scan.issue.filter.FilterableIssue; import org.sonar.api.scan.issue.filter.IssueFilter; import org.sonar.api.scan.issue.filter.IssueFilterChain; public class DefaultIssueFilterChain implements IssueFilterChain { private final List filters; public DefaultIssueFilterChain(List filters) { this.filters = filters; } @Override public boolean accept(FilterableIssue issue) { if (filters.isEmpty()) { return true; } else { return filters.get(0).accept(issue, new DefaultIssueFilterChain(filters.subList(1, filters.size()))); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/IssueFilters.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import java.util.List; import java.util.Optional; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.scan.issue.filter.FilterableIssue; import org.sonar.api.scan.issue.filter.IssueFilter; import org.sonar.api.scan.issue.filter.IssueFilterChain; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultFilterableIssue; @SonarLintSide public class IssueFilters { private final List filters; public IssueFilters(Optional> exclusionFilters) { this.filters = exclusionFilters.orElse(List.of()); } public boolean accept(InputComponent inputComponent, Issue rawIssue) { IssueFilterChain filterChain = new DefaultIssueFilterChain(filters); FilterableIssue fIssue = new DefaultFilterableIssue(rawIssue, inputComponent); return filterChain.accept(fIssue); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/SensorInputFileEdit.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.issue.fix.InputFileEdit; import org.sonar.api.batch.sensor.issue.fix.NewInputFileEdit; import org.sonar.api.batch.sensor.issue.fix.NewTextEdit; import org.sonar.api.batch.sensor.issue.fix.TextEdit; public class SensorInputFileEdit implements InputFileEdit, NewInputFileEdit, org.sonarsource.sonarlint.plugin.api.issue.NewInputFileEdit { private final List textEdits = new ArrayList<>(); private InputFile inputFile; @Override public SensorInputFileEdit on(InputFile inputFile) { this.inputFile = inputFile; return this; } @Override public SensorTextEdit newTextEdit() { return new SensorTextEdit(); } @Override public NewInputFileEdit addTextEdit(NewTextEdit newTextEdit) { textEdits.add((SensorTextEdit) newTextEdit); return this; } @Override public org.sonarsource.sonarlint.plugin.api.issue.NewInputFileEdit addTextEdit(org.sonarsource.sonarlint.plugin.api.issue.NewTextEdit newTextEdit) { // legacy method from sonarlint-plugin-api, keep for backward compatibility and remove later textEdits.add((SensorTextEdit) newTextEdit); return this; } @Override public InputFile target() { return inputFile; } @Override public List textEdits() { return Collections.unmodifiableList(textEdits); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/SensorQuickFix.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import java.util.ArrayList; import java.util.List; import org.sonar.api.batch.sensor.issue.fix.InputFileEdit; import org.sonar.api.batch.sensor.issue.fix.NewInputFileEdit; import org.sonar.api.batch.sensor.issue.fix.NewQuickFix; import org.sonar.api.batch.sensor.issue.fix.QuickFix; public class SensorQuickFix implements QuickFix, NewQuickFix, org.sonarsource.sonarlint.plugin.api.issue.NewQuickFix { private final List inputFileEdits = new ArrayList<>(); private String message; @Override public SensorQuickFix message(String message) { this.message = message; return this; } @Override public SensorInputFileEdit newInputFileEdit() { return new SensorInputFileEdit(); } @Override public NewQuickFix addInputFileEdit(NewInputFileEdit newInputFileEdit) { inputFileEdits.add((SensorInputFileEdit) newInputFileEdit); return this; } @Override public SensorQuickFix addInputFileEdit(org.sonarsource.sonarlint.plugin.api.issue.NewInputFileEdit newInputFileEdit) { // legacy method from sonarlint-plugin-api, keep for backward compatibility and remove later inputFileEdits.add((SensorInputFileEdit) newInputFileEdit); return this; } @Override public String message() { return message; } @Override public List inputFileEdits() { return inputFileEdits; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/SensorTextEdit.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.issue.fix.NewTextEdit; import org.sonar.api.batch.sensor.issue.fix.TextEdit; public class SensorTextEdit implements TextEdit, NewTextEdit, org.sonarsource.sonarlint.plugin.api.issue.NewTextEdit { private TextRange range; private String newText; @Override public SensorTextEdit at(TextRange range) { this.range = range; return this; } @Override public SensorTextEdit withNewText(String newText) { this.newText = newText; return this; } @Override public TextRange range() { return range; } @Override public String newText() { return newText; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/TextRangeUtils.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import org.sonarsource.sonarlint.core.commons.api.TextRange; public class TextRangeUtils { private TextRangeUtils() { } public static TextRange convert(org.sonar.api.batch.fs.TextRange analyzerTextRange) { return new TextRange( analyzerTextRange.start().line(), analyzerTextRange.start().lineOffset(), analyzerTextRange.end().line(), analyzerTextRange.end().lineOffset()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/EnforceIssuesFilter.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.sonar.api.scan.issue.filter.FilterableIssue; import org.sonar.api.scan.issue.filter.IssueFilter; import org.sonar.api.scan.issue.filter.IssueFilterChain; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueInclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssuePattern; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultFilterableIssue; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class EnforceIssuesFilter implements IssueFilter { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final List multicriteriaPatterns; public EnforceIssuesFilter(IssueInclusionPatternInitializer patternInitializer) { this.multicriteriaPatterns = Collections.unmodifiableList(new ArrayList<>(patternInitializer.getMulticriteriaPatterns())); } @Override public boolean accept(FilterableIssue issue, IssueFilterChain chain) { var atLeastOneRuleMatched = false; var atLeastOnePatternFullyMatched = false; IssuePattern matchingPattern = null; for (IssuePattern pattern : multicriteriaPatterns) { if (pattern.matchRule(issue.ruleKey())) { atLeastOneRuleMatched = true; var component = ((DefaultFilterableIssue) issue).getComponent(); if (component.isFile()) { var file = (SonarLintInputFile) component; if (pattern.matchFile(file.relativePath())) { atLeastOnePatternFullyMatched = true; matchingPattern = pattern; } } } } if (atLeastOneRuleMatched) { if (atLeastOnePatternFullyMatched) { LOG.debug("Issue {} enforced by pattern {}", issue, matchingPattern); } return atLeastOnePatternFullyMatched; } else { return chain.accept(issue); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/IgnoreIssuesFilter.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.scan.issue.filter.FilterableIssue; import org.sonar.api.scan.issue.filter.IssueFilter; import org.sonar.api.scan.issue.filter.IssueFilterChain; import org.sonar.api.utils.WildcardPattern; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultFilterableIssue; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class IgnoreIssuesFilter implements IssueFilter { private final Map> rulePatternByComponent = new HashMap<>(); private static final SonarLintLogger LOG = SonarLintLogger.get(); @Override public boolean accept(FilterableIssue issue, IssueFilterChain chain) { var component = ((DefaultFilterableIssue) issue).getComponent(); if ((component.isFile() && ((SonarLintInputFile) component).isIgnoreAllIssues()) || (component.isFile() && ((SonarLintInputFile) component).isIgnoreAllIssuesOnLine(issue.line()))) { return false; } if (hasRuleMatchFor(component, issue)) { return false; } return chain.accept(issue); } public void addRuleExclusionPatternForComponent(SonarLintInputFile inputFile, WildcardPattern rulePattern) { if ("*".equals(rulePattern.toString())) { inputFile.setIgnoreAllIssues(true); } else { rulePatternByComponent.computeIfAbsent(inputFile, x -> new LinkedList<>()).add(rulePattern); } } private boolean hasRuleMatchFor(InputComponent component, FilterableIssue issue) { for (WildcardPattern pattern : rulePatternByComponent.getOrDefault(component, Collections.emptyList())) { if (pattern.match(issue.ruleKey().toString())) { LOG.debug("Issue {} ignored by exclusion pattern {}", issue, pattern); return true; } } return false; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/SonarLintNoSonarFilter.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import java.util.Set; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.issue.NoSonarFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; public class SonarLintNoSonarFilter extends NoSonarFilter { @Override public NoSonarFilter noSonarInFile(InputFile inputFile, Set noSonarLines) { ((SonarLintInputFile) inputFile).noSonarAt(noSonarLines); return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/AbstractPatternInitializer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public abstract class AbstractPatternInitializer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Configuration config; private List multicriteriaPatterns; protected AbstractPatternInitializer(Configuration config) { this.config = config; initPatterns(); } protected Configuration getSettings() { return config; } public List getMulticriteriaPatterns() { return multicriteriaPatterns; } public boolean hasConfiguredPatterns() { return hasMulticriteriaPatterns(); } public boolean hasMulticriteriaPatterns() { return !multicriteriaPatterns.isEmpty(); } protected final void initPatterns() { // Patterns Multicriteria multicriteriaPatterns = new ArrayList<>(); for (String id : config.getStringArray(getMulticriteriaConfigurationKey())) { var propPrefix = getMulticriteriaConfigurationKey() + "." + id + "."; var filePathPattern = config.get(propPrefix + "resourceKey").orElse(null); if (StringUtils.isBlank(filePathPattern)) { LOG.debug("Issue exclusions are misconfigured. File pattern is mandatory for each entry of '" + getMulticriteriaConfigurationKey() + "'"); continue; } var ruleKeyPattern = config.get(propPrefix + "ruleKey").orElse(null); if (StringUtils.isBlank(ruleKeyPattern)) { LOG.debug("Issue exclusions are misconfigured. Rule key pattern is mandatory for each entry of '" + getMulticriteriaConfigurationKey() + "'"); continue; } var pattern = new IssuePattern(filePathPattern, ruleKeyPattern); multicriteriaPatterns.add(pattern); } } protected abstract String getMulticriteriaConfigurationKey(); } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/BlockIssuePattern.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; public class BlockIssuePattern { private final String beginBlockRegexp; private final String endBlockRegexp; public BlockIssuePattern(String beginBlockRegexp, String endBlockRegexp) { this.beginBlockRegexp = beginBlockRegexp; this.endBlockRegexp = endBlockRegexp; } public String getBeginBlockRegexp() { return beginBlockRegexp; } public String getEndBlockRegexp() { return endBlockRegexp; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssueExclusionPatternInitializer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class IssueExclusionPatternInitializer extends AbstractPatternInitializer { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String EXCLUSION_KEY_PREFIX = "sonar.issue.ignore"; public static final String BLOCK_SUFFIX = ".block"; public static final String PATTERNS_BLOCK_KEY = EXCLUSION_KEY_PREFIX + BLOCK_SUFFIX; public static final String BEGIN_BLOCK_REGEXP = "beginBlockRegexp"; public static final String END_BLOCK_REGEXP = "endBlockRegexp"; public static final String ALLFILE_SUFFIX = ".allfile"; public static final String PATTERNS_ALLFILE_KEY = EXCLUSION_KEY_PREFIX + ALLFILE_SUFFIX; public static final String FILE_REGEXP = "fileRegexp"; private List blockPatterns; private List allFilePatterns; public IssueExclusionPatternInitializer(Configuration config) { super(config); loadFileContentPatterns(); } @Override protected String getMulticriteriaConfigurationKey() { return EXCLUSION_KEY_PREFIX + ".multicriteria"; } @Override public boolean hasConfiguredPatterns() { return hasFileContentPattern() || hasMulticriteriaPatterns(); } private void loadFileContentPatterns() { // Patterns Block blockPatterns = new ArrayList<>(); for (String id : getSettings().getStringArray(PATTERNS_BLOCK_KEY)) { var propPrefix = PATTERNS_BLOCK_KEY + "." + id + "."; var beginBlockRegexp = getSettings().get(propPrefix + BEGIN_BLOCK_REGEXP).orElse(null); if (StringUtils.isBlank(beginBlockRegexp)) { LOG.debug("Issue exclusions are misconfigured. Start block regexp is mandatory for each entry of '" + PATTERNS_BLOCK_KEY + "'"); continue; } var endBlockRegexp = getSettings().get(propPrefix + END_BLOCK_REGEXP).orElse(null); // As per configuration help, missing second field means: from start regexp to EOF var pattern = new BlockIssuePattern(nullToEmpty(beginBlockRegexp), nullToEmpty(endBlockRegexp)); blockPatterns.add(pattern); } blockPatterns = Collections.unmodifiableList(blockPatterns); // Patterns All File allFilePatterns = new ArrayList<>(); for (String id : getSettings().getStringArray(PATTERNS_ALLFILE_KEY)) { var propPrefix = PATTERNS_ALLFILE_KEY + "." + id + "."; var allFileRegexp = getSettings().get(propPrefix + FILE_REGEXP).orElse(null); if (StringUtils.isBlank(allFileRegexp)) { LOG.debug("Issue exclusions are misconfigured. Remove blank entries from '" + PATTERNS_ALLFILE_KEY + "'"); continue; } allFilePatterns.add(nullToEmpty(allFileRegexp)); } allFilePatterns = Collections.unmodifiableList(allFilePatterns); } private static String nullToEmpty(@Nullable String str) { if (str == null) { return ""; } return str; } public List getBlockPatterns() { return blockPatterns; } public List getAllFilePatterns() { return allFilePatterns; } public boolean hasFileContentPattern() { return !(blockPatterns.isEmpty() && allFilePatterns.isEmpty()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssueInclusionPatternInitializer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import org.sonar.api.config.Configuration; public class IssueInclusionPatternInitializer extends AbstractPatternInitializer { public static final String INCLUSION_KEY_PREFIX = "sonar.issue.enforce"; public IssueInclusionPatternInitializer(Configuration config) { super(config); } @Override protected String getMulticriteriaConfigurationKey() { return INCLUSION_KEY_PREFIX + ".multicriteria"; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssuePattern.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import javax.annotation.Nullable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.WildcardPattern; import org.sonarsource.sonarlint.core.analysis.container.analysis.SonarLintPathPattern; public class IssuePattern { private final SonarLintPathPattern pathPattern; private final WildcardPattern rulePattern; public IssuePattern(String pathPattern, String rulePattern) { this.pathPattern = new SonarLintPathPattern(pathPattern); this.rulePattern = WildcardPattern.create(rulePattern); } public WildcardPattern getRulePattern() { return rulePattern; } public boolean matchRule(RuleKey rule) { return rulePattern.match(rule.toString()); } public boolean matchFile(@Nullable String filePath) { return filePath != null && pathPattern.match(filePath); } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/IssueExclusionsLoader.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import java.util.ArrayList; import java.util.List; import javax.annotation.CheckForNull; import org.apache.commons.lang3.StringUtils; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.CharHandler; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.IgnoreIssuesFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.BlockIssuePattern; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueExclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssuePattern; public class IssueExclusionsLoader { private final List allFilePatterns; private final List blockMatchers; private final IgnoreIssuesFilter ignoreIssuesFilter; private final IssueExclusionPatternInitializer patternsInitializer; private final boolean enableCharHandler; public IssueExclusionsLoader(IssueExclusionPatternInitializer patternsInitializer, IgnoreIssuesFilter ignoreIssuesFilter) { this.patternsInitializer = patternsInitializer; this.ignoreIssuesFilter = ignoreIssuesFilter; this.allFilePatterns = new ArrayList<>(); this.blockMatchers = new ArrayList<>(); for (String pattern : patternsInitializer.getAllFilePatterns()) { allFilePatterns.add(java.util.regex.Pattern.compile(pattern)); } for (BlockIssuePattern pattern : patternsInitializer.getBlockPatterns()) { blockMatchers.add(new DoubleRegexpMatcher( java.util.regex.Pattern.compile(pattern.getBeginBlockRegexp()), java.util.regex.Pattern.compile(pattern.getEndBlockRegexp()))); } enableCharHandler = !allFilePatterns.isEmpty() || !blockMatchers.isEmpty(); } public void addMulticriteriaPatterns(SonarLintInputFile inputFile) { for (IssuePattern pattern : patternsInitializer.getMulticriteriaPatterns()) { if (pattern.matchFile(inputFile.relativePath())) { ignoreIssuesFilter.addRuleExclusionPatternForComponent(inputFile, pattern.getRulePattern()); } } } @CheckForNull public CharHandler createCharHandlerFor(SonarLintInputFile inputFile) { if (enableCharHandler) { return new IssueExclusionsRegexpScanner(inputFile, allFilePatterns, blockMatchers); } return null; } public static class DoubleRegexpMatcher { private final java.util.regex.Pattern firstPattern; private final java.util.regex.Pattern secondPattern; DoubleRegexpMatcher(java.util.regex.Pattern firstPattern, java.util.regex.Pattern secondPattern) { this.firstPattern = firstPattern; this.secondPattern = secondPattern; } boolean matchesFirstPattern(String line) { return firstPattern.matcher(line).find(); } boolean matchesSecondPattern(String line) { return hasSecondPattern() && secondPattern.matcher(line).find(); } boolean hasSecondPattern() { return StringUtils.isNotEmpty(secondPattern.toString()); } } @Override public String toString() { return "Issues Exclusions - Source Scanner"; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/IssueExclusionsRegexpScanner.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.CharHandler; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader.DoubleRegexpMatcher; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class IssueExclusionsRegexpScanner extends CharHandler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StringBuilder sb = new StringBuilder(); private final List allFilePatterns; private final List blockMatchers; private final SonarLintInputFile inputFile; private int lineIndex = 1; private final List lineExclusions = new ArrayList<>(); private LineExclusion currentLineExclusion = null; private int fileLength = 0; private DoubleRegexpMatcher currentMatcher; private boolean ignoreAllIssues; IssueExclusionsRegexpScanner(SonarLintInputFile inputFile, List allFilePatterns, List blockMatchers) { this.allFilePatterns = allFilePatterns; this.blockMatchers = blockMatchers; this.inputFile = inputFile; LOG.debug("Evaluate issue exclusions for '{}'", inputFile.relativePath()); } @Override public void handleIgnoreEoL(char c) { if (ignoreAllIssues) { // Optimization return; } sb.append(c); } @Override public void newLine() { if (ignoreAllIssues) { // Optimization return; } processLine(sb.toString()); sb.setLength(0); lineIndex++; } @Override public void eof() { if (ignoreAllIssues) { // Optimization return; } processLine(sb.toString()); if (currentMatcher != null && !currentMatcher.hasSecondPattern()) { // this will happen when there is a start block regexp but no end block regexp endExclusion(lineIndex + 1); } // now create the new line-based pattern for this file if there are exclusions fileLength = lineIndex; if (!lineExclusions.isEmpty()) { var lineRanges = convertLineExclusionsToLineRanges(); LOG.debug(" - Line exclusions found: {}", lineRanges.stream().map(LineRange::toString).collect(Collectors.joining(","))); inputFile.addIgnoreIssuesOnLineRanges(lineRanges.stream().map(r -> new int[] {r.from(), r.to()}).toList()); } } private void processLine(String line) { if (line.trim().isEmpty()) { return; } // first check the single regexp patterns that can be used to totally exclude a file for (Pattern pattern : allFilePatterns) { if (pattern.matcher(line).find()) { // nothing more to do on this file LOG.debug(" - Exclusion pattern '{}': all issues in this file will be ignored.", pattern); ignoreAllIssues = true; inputFile.setIgnoreAllIssues(true); return; } } // then check the double regexps if we're still here checkDoubleRegexps(line, lineIndex); } private Set convertLineExclusionsToLineRanges() { Set lineRanges = HashSet.newHashSet(lineExclusions.size()); for (LineExclusion lineExclusion : lineExclusions) { lineRanges.add(lineExclusion.toLineRange(fileLength)); } return lineRanges; } private void checkDoubleRegexps(String line, int lineIndex) { if (currentMatcher == null) { for (DoubleRegexpMatcher matcher : blockMatchers) { if (matcher.matchesFirstPattern(line)) { startExclusion(lineIndex); currentMatcher = matcher; break; } } } else { if (currentMatcher.matchesSecondPattern(line)) { endExclusion(lineIndex); currentMatcher = null; } } } private void startExclusion(int lineIndex) { currentLineExclusion = new LineExclusion(lineIndex); lineExclusions.add(currentLineExclusion); } private void endExclusion(int lineIndex) { currentLineExclusion.setEnd(lineIndex); currentLineExclusion = null; } private static class LineExclusion { private final int start; private int end; LineExclusion(int start) { this.start = start; this.end = -1; } void setEnd(int end) { this.end = end; } public LineRange toLineRange(int fileLength) { return new LineRange(start, end == -1 ? fileLength : end); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/LineRange.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; public class LineRange { private final int from; private final int to; public LineRange(int line) { this(line, line); } public LineRange(int from, int to) { if (from > to) { throw new IllegalArgumentException(String.format("Line range is not valid: %s must be greater or equal than %s", to, from)); } this.from = from; this.to = to; } public boolean in(int lineId) { return from <= lineId && lineId <= to; } public Set toLines() { Set lines = LinkedHashSet.newLinkedHashSet(to - from + 1); for (var index = from; index <= to; index++) { lines.add(index); } return lines; } public int from() { return from; } public int to() { return to; } @Override public String toString() { return "[" + from + "-" + to + "]"; } @Override public int hashCode() { return Objects.hash(from, to); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if ((obj == null) || (getClass() != obj.getClass())) { return false; } return !fieldsDiffer((LineRange) obj); } private boolean fieldsDiffer(LineRange other) { return from != other.from || to != other.to; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.issue; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SensorOptimizer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.config.Configuration; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorDescriptor; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; @SonarLintSide public class SensorOptimizer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final FileSystem fs; private final ActiveRules activeRules; private final Configuration config; public SensorOptimizer(FileSystem fs, ActiveRules activeRules, Configuration config) { this.fs = fs; this.activeRules = activeRules; this.config = config; } /** * Decide if the given Sensor should be executed. */ public boolean shouldExecute(DefaultSensorDescriptor descriptor) { if (!fsCondition(descriptor)) { LOG.debug("'{}' skipped because there are no related files in the current project", descriptor.name()); return false; } if (!activeRulesCondition(descriptor)) { LOG.debug("'{}' skipped because there are no related rules activated", descriptor.name()); return false; } if (!settingsCondition(descriptor)) { LOG.debug("'{}' skipped because one of the required properties is missing", descriptor.name()); return false; } return true; } private boolean settingsCondition(DefaultSensorDescriptor descriptor) { if (descriptor.configurationPredicate() != null) { return descriptor.configurationPredicate().test(config); } return true; } private boolean activeRulesCondition(DefaultSensorDescriptor descriptor) { if (!descriptor.ruleRepositories().isEmpty()) { for (String repoKey : descriptor.ruleRepositories()) { if (!activeRules.findByRepository(repoKey).isEmpty()) { return true; } } return false; } return true; } private boolean fsCondition(DefaultSensorDescriptor descriptor) { if (!descriptor.languages().isEmpty() || descriptor.type() != null) { var langPredicate = descriptor.languages().isEmpty() ? fs.predicates().all() : fs.predicates().hasLanguages(descriptor.languages()); var typePredicate = descriptor.type() == null ? fs.predicates().all() : fs.predicates().hasType(descriptor.type()); return fs.hasFiles(fs.predicates().and(langPredicate, typePredicate)); } return true; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SensorsExecutor.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonar.api.batch.DependedUpon; import org.sonar.api.batch.DependsUpon; import org.sonar.api.batch.Phase; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.scanner.sensor.ProjectSensor; import org.sonar.api.utils.AnnotationUtils; import org.sonar.api.utils.dag.DirectAcyclicGraph; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorContext; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorDescriptor; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import static org.sonarsource.sonarlint.core.commons.tracing.Trace.startChild; /** * Execute Sensors. */ public class SensorsExecutor { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SensorOptimizer sensorOptimizer; private final List sensors; private final DefaultSensorContext context; @Nullable private final Trace trace; public SensorsExecutor(DefaultSensorContext context, SensorOptimizer sensorOptimizer, Optional trace, Optional> sensors) { this.context = context; this.sensors = sensors.orElse(List.of()); this.sensorOptimizer = sensorOptimizer; this.trace = trace.orElse(null); } public void execute() { var sensorGroups = sensors.stream().collect(Collectors.partitioningBy(s -> { var isModernGlobalSensor = !(s instanceof Sensor); if (isModernGlobalSensor) { return true; } else { var descriptor = new DefaultSensorDescriptor(); s.describe(descriptor); return descriptor.isGlobal(); } })); var moduleSensors = sensorGroups.get(false); var globalSensors = sensorGroups.get(true); executeSensors(moduleSensors); executeSensors(globalSensors); } private void executeSensors(List sensors) { for (var sensor : sort(sensors)) { if (context.isCancelled()) { LOG.debug("Analysis is canceled"); return; } var descriptor = new DefaultSensorDescriptor(); sensor.describe(descriptor); if (sensorOptimizer.shouldExecute(descriptor)) { executeSensor(context, sensor, descriptor, trace); } } } private static void executeSensor(SensorContext context, ProjectSensor sensor, DefaultSensorDescriptor descriptor, @Nullable Trace trace) { var sensorName = descriptor.name() != null ? descriptor.name() : describe(sensor); LOG.debug("Execute Sensor: {}", sensorName); try { startChild(trace, "executeSensor", sensorName, () -> sensor.execute(context)); } catch (Throwable t) { LOG.error("Error executing sensor: '{}'", sensorName, t); } } static String describe(Object o) { try { if (o.getClass().getMethod("toString").getDeclaringClass() != Object.class) { var str = o.toString(); if (str != null) { return str; } } } catch (Exception e) { // fallback } return o.getClass().getName(); } private static Collection sort(Collection extensions) { var dag = new DirectAcyclicGraph(); for (T extension : extensions) { dag.add(extension); for (Object dependency : getDependencies(extension)) { dag.add(extension, dependency); } for (Object generates : getDependents(extension)) { dag.add(generates, extension); } completePhaseDependencies(dag, extension); } List sortedList = dag.sort(); return (Collection) sortedList.stream() .filter(extensions::contains) .toList(); } /** * Extension dependencies */ private static List getDependencies(T extension) { return new ArrayList<>(evaluateAnnotatedClasses(extension, DependsUpon.class)); } /** * Objects that depend upon this extension. */ private static List getDependents(T extension) { return new ArrayList<>(evaluateAnnotatedClasses(extension, DependedUpon.class)); } private static void completePhaseDependencies(DirectAcyclicGraph dag, Object extension) { var phase = evaluatePhase(extension); dag.add(extension, phase); for (Phase.Name name : Phase.Name.values()) { if (phase.compareTo(name) < 0) { dag.add(name, extension); } else if (phase.compareTo(name) > 0) { dag.add(extension, name); } } } private static Phase.Name evaluatePhase(Object extension) { var phaseAnnotation = AnnotationUtils.getAnnotation(extension, Phase.class); if (phaseAnnotation != null) { return phaseAnnotation.name(); } return Phase.Name.DEFAULT; } static List evaluateAnnotatedClasses(Object extension, Class annotation) { List results = new ArrayList<>(); Class aClass = extension.getClass(); while (aClass != null) { evaluateClass(aClass, annotation, results); aClass = aClass.getSuperclass(); } return results; } private static void evaluateClass(Class extensionClass, Class annotationClass, List results) { Annotation annotation = extensionClass.getAnnotation(annotationClass); if (annotation != null) { if (annotation.annotationType().isAssignableFrom(DependsUpon.class)) { results.addAll(Arrays.asList(((DependsUpon) annotation).value())); } else if (annotation.annotationType().isAssignableFrom(DependedUpon.class)) { results.addAll(Arrays.asList(((DependedUpon) annotation).value())); } } var interfaces = extensionClass.getInterfaces(); for (Class anInterface : interfaces) { evaluateClass(anInterface, annotationClass, results); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SonarLintSensorStorage.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.lang3.Strings; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.sensor.code.NewSignificantCode; import org.sonar.api.batch.sensor.coverage.NewCoverage; import org.sonar.api.batch.sensor.cpd.NewCpdTokens; import org.sonar.api.batch.sensor.error.AnalysisError; import org.sonar.api.batch.sensor.highlighting.NewHighlighting; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.batch.sensor.issue.ExternalIssue; import org.sonar.api.batch.sensor.issue.Issue; import org.sonar.api.batch.sensor.issue.Issue.Flow; import org.sonar.api.batch.sensor.issue.fix.QuickFix; import org.sonar.api.batch.sensor.measure.Measure; import org.sonar.api.batch.sensor.rule.AdHocRule; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.issue.impact.Severity; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFileEdit; import org.sonarsource.sonarlint.core.analysis.api.TextEdit; import org.sonarsource.sonarlint.core.analysis.container.analysis.IssueListenerHolder; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.IssueFilters; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.TextRangeUtils; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSonarLintIssue; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; public class SonarLintSensorStorage implements SensorStorage { private final ActiveRules activeRules; private final IssueFilters filters; private final IssueListenerHolder issueListener; private final AnalysisResults analysisResult; public SonarLintSensorStorage(ActiveRules activeRules, IssueFilters filters, IssueListenerHolder issueListener, AnalysisResults analysisResult) { this.activeRules = activeRules; this.filters = filters; this.issueListener = issueListener; this.analysisResult = analysisResult; } @Override public void store(Measure newMeasure) { // NO-OP } @Override public void store(Issue issue) { if (!(issue instanceof DefaultSonarLintIssue sonarLintIssue)) { throw new IllegalArgumentException("Trying to store a non-SonarLint issue?"); } var inputComponent = sonarLintIssue.primaryLocation().inputComponent(); var activeRule = activeRules.find(sonarLintIssue.ruleKey()); if ((activeRule == null) || noSonar(inputComponent, sonarLintIssue)) { return; } var primaryMessage = sonarLintIssue.primaryLocation().message(); var flows = mapFlows(sonarLintIssue.flows()); var quickFixes = transform(sonarLintIssue.quickFixes()); var overriddenImpacts = transform(sonarLintIssue.overridenImpacts()); var newIssue = new org.sonarsource.sonarlint.core.analysis.api.Issue(activeRule, primaryMessage, overriddenImpacts, issue.primaryLocation().textRange(), inputComponent.isFile() ? ((SonarLintInputFile) inputComponent).getClientInputFile() : null, flows, quickFixes, sonarLintIssue.ruleDescriptionContextKey()); if (filters.accept(inputComponent, newIssue)) { issueListener.handle(newIssue); } } private static List transform(List quickFixes) { return quickFixes.stream().map(SonarLintSensorStorage::transform).toList(); } private static Map transform(Map overriddenImpacts) { return overriddenImpacts.entrySet().stream() .map(e -> Map.entry(SoftwareQuality.valueOf(e.getKey().name()), ImpactSeverity.valueOf(e.getValue().name()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } private static org.sonarsource.sonarlint.core.analysis.api.QuickFix transform(QuickFix qf) { return new org.sonarsource.sonarlint.core.analysis.api.QuickFix( qf.inputFileEdits().stream().map(edit -> new ClientInputFileEdit( ((SonarLintInputFile) edit.target()).getClientInputFile(), edit.textEdits().stream().map(textEdit -> new TextEdit(TextRangeUtils.convert(textEdit.range()), textEdit.newText())).toList())).toList(), qf.message()); } private static boolean noSonar(InputComponent inputComponent, Issue issue) { var textRange = issue.primaryLocation().textRange(); return inputComponent.isFile() && textRange != null && ((SonarLintInputFile) inputComponent).hasNoSonarAt(textRange.start().line()) && !Strings.CI.contains(issue.ruleKey().rule(), "nosonar"); } private static List mapFlows(List flows) { return flows.stream() .map(f -> new org.sonarsource.sonarlint.core.analysis.api.Flow(new ArrayList<>(f.locations()))) .filter(f -> !f.locations().isEmpty()) .toList(); } @Override public void store(NewHighlighting highlighting) { // NO-OP } @Override public void store(NewCoverage defaultCoverage) { // NO-OP } @Override public void store(NewCpdTokens defaultCpdTokens) { // NO-OP } @Override public void store(NewSymbolTable symbolTable) { // NO-OP } @Override public void store(AnalysisError analysisError) { var clientInputFile = ((SonarLintInputFile) analysisError.inputFile()).getClientInputFile(); analysisResult.addFailedAnalysisFile(clientInputFile); } @Override public void storeProperty(String key, String value) { // NO-OP } @Override public void store(ExternalIssue issue) { // NO-OP } @Override public void store(NewSignificantCode significantCode) { // NO-OP } @Override public void store(AdHocRule adHocRule) { // NO-OP } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/AnalysisExtensionInstaller.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.config.Configuration; import org.sonar.api.utils.AnnotationUtils; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.analysis.container.ContainerLifespan; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.commons.ExtensionInstaller; import org.sonarsource.sonarlint.core.plugin.commons.ExtensionUtils; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; import org.sonarsource.sonarlint.core.plugin.commons.container.ExtensionContainer; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; public class AnalysisExtensionInstaller extends ExtensionInstaller { private final LoadedPlugins loadedPlugins; public AnalysisExtensionInstaller(SonarLintRuntime sonarRuntime, LoadedPlugins loadedPlugins, Configuration bootConfiguration) { super(sonarRuntime, bootConfiguration); this.loadedPlugins = loadedPlugins; } public void install(ExtensionContainer container, ContainerLifespan lifespan) { super.install(container, loadedPlugins.getAnalysisPluginInstancesByKeys(), (pluginKey, extension) -> lifespan.equals(getSonarLintSideLifespan(extension)) && onlySonarSourceSensor(pluginKey, extension)); } private static ContainerLifespan getSonarLintSideLifespan(Object extension) { var annotation = AnnotationUtils.getAnnotation(extension, SonarLintSide.class); if (annotation != null) { var lifespan = annotation.lifespan(); if (SonarLintSide.MULTIPLE_ANALYSES.equals(lifespan) || "INSTANCE".equals(lifespan)) { return ContainerLifespan.INSTANCE; } if ("MODULE".equals(lifespan)) { return ContainerLifespan.MODULE; } if (SonarLintSide.SINGLE_ANALYSIS.equals(lifespan)) { return ContainerLifespan.ANALYSIS; } } return null; } private boolean onlySonarSourceSensor(String pluginKey, Object extension) { return SonarPlugin.findByKey(pluginKey).isPresent() || loadedPlugins.getAdditionalAllowedPlugins().contains(pluginKey) || isNotSensor(extension); } private static boolean isNotSensor(Object extension) { return !ExtensionUtils.isType(extension, Sensor.class); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalAnalysisContainer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.time.Clock; import org.sonar.api.SonarQubeVersion; import org.sonar.api.utils.System2; import org.sonar.api.utils.UriReader; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.ApiVersions; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.SonarLintRuntimeImpl; public class GlobalAnalysisContainer extends SpringComponentContainer { protected static final SonarLintLogger LOG = SonarLintLogger.get(); private GlobalExtensionContainer globalExtensionContainer; private ModuleRegistry moduleRegistry; private final AnalysisSchedulerConfiguration analysisGlobalConfig; private final LoadedPlugins loadedPlugins; public GlobalAnalysisContainer(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins) { this.analysisGlobalConfig = analysisGlobalConfig; this.loadedPlugins = loadedPlugins; } @Override protected void doBeforeStart() { var sonarPluginApiVersion = ApiVersions.loadSonarPluginApiVersion(); var sonarlintPluginApiVersion = ApiVersions.loadSonarLintPluginApiVersion(); add( analysisGlobalConfig, loadedPlugins, GlobalSettings.class, new GlobalConfigurationProvider(), AnalysisExtensionInstaller.class, new SonarQubeVersion(sonarPluginApiVersion), new SonarLintRuntimeImpl(sonarPluginApiVersion, sonarlintPluginApiVersion, analysisGlobalConfig.getClientPid()), new GlobalTempFolderProvider(), UriReader.class, Clock.systemDefaultZone(), System2.INSTANCE); } @Override protected void doAfterStart() { declarePluginProperties(); globalExtensionContainer = new GlobalExtensionContainer(this); globalExtensionContainer.startComponents(); this.moduleRegistry = new ModuleRegistry(globalExtensionContainer, analysisGlobalConfig.getFileSystemProvider()); } @Override public SpringComponentContainer stopComponents() { try { if (moduleRegistry != null) { moduleRegistry.stopAll(); } if (globalExtensionContainer != null) { globalExtensionContainer.stopComponents(); } } catch (Exception e) { LOG.error("Cannot close analysis engine", e); } finally { super.stopComponents(); } return this; } private void declarePluginProperties() { loadedPlugins.getAnalysisPluginInstancesByKeys().values().forEach(this::declareProperties); } // Visible for medium tests public ModuleRegistry getModuleRegistry() { return moduleRegistry; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalConfigurationProvider.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.ConfigurationBridge; import org.springframework.context.annotation.Bean; public class GlobalConfigurationProvider { private Configuration globalConfig; @Bean("configuration") public Configuration provide(GlobalSettings settings) { if (globalConfig == null) { this.globalConfig = new ConfigurationBridge(settings); } return globalConfig; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalExtensionContainer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import org.sonarsource.sonarlint.core.analysis.container.ContainerLifespan; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; /** * Used to load plugin global extensions */ public class GlobalExtensionContainer extends SpringComponentContainer { public GlobalExtensionContainer(SpringComponentContainer parent) { super(parent); } @Override protected void doBeforeStart() { getParent().getComponentByType(AnalysisExtensionInstaller.class).install(this, ContainerLifespan.INSTANCE); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalSettings.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import org.sonar.api.config.PropertyDefinitions; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; public class GlobalSettings extends MapSettings { public GlobalSettings(AnalysisSchedulerConfiguration config, PropertyDefinitions propertyDefinitions) { super(propertyDefinitions, config.getEffectiveSettings()); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalTempFolder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.io.File; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultTempFolder; public class GlobalTempFolder extends DefaultTempFolder { public GlobalTempFolder(File tempDir, boolean deleteOnExit) { super(tempDir, deleteOnExit); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalTempFolderProvider.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.springframework.context.annotation.Bean; public class GlobalTempFolderProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final long CLEAN_MAX_AGE = TimeUnit.DAYS.toMillis(7); private static final String TMP_NAME_PREFIX = ".sonarlinttmp_"; private GlobalTempFolder tempFolder; @Bean("globalTempFolder") public GlobalTempFolder provide(AnalysisSchedulerConfiguration globalConfiguration) { if (tempFolder == null) { tempFolder = cleanAndCreateTempFolder(globalConfiguration.getWorkDir()); } return tempFolder; } private static GlobalTempFolder cleanAndCreateTempFolder(Path workingPath) { try { cleanTempFolders(workingPath); } catch (IOException e) { LOG.error(String.format("failed to clean global working directory: %s", workingPath), e); } var tempDir = createTempFolder(workingPath); return new GlobalTempFolder(tempDir.toFile(), true); } private static Path createTempFolder(Path workingPath) { try { Files.createDirectories(workingPath); } catch (IOException e) { throw new IllegalStateException("Failed to create working path: " + workingPath, e); } try { return Files.createTempDirectory(workingPath, TMP_NAME_PREFIX); } catch (IOException e) { throw new IllegalStateException("Failed to create temporary folder in " + workingPath, e); } } private static void cleanTempFolders(Path path) throws IOException { if (Files.exists(path)) { try (var stream = Files.newDirectoryStream(path, new CleanFilter())) { for (Path p : stream) { FileUtils.deleteQuietly(p.toFile()); } } } } private static class CleanFilter implements DirectoryStream.Filter { @Override public boolean accept(Path path) throws IOException { if (!Files.isDirectory(path) || !path.getFileName().toString().startsWith(TMP_NAME_PREFIX)) { return false; } var threshold = System.currentTimeMillis() - CLEAN_MAX_AGE; // we could also check the timestamp in the name, instead BasicFileAttributes attrs; try { attrs = Files.readAttributes(path, BasicFileAttributes.class); } catch (IOException ioe) { LOG.error(String.format("Couldn't read file attributes for %s : ", path), ioe); return false; } var creationTime = attrs.creationTime().toMillis(); return creationTime < threshold; } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/ModuleRegistry.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileSystem; import org.sonarsource.sonarlint.core.analysis.container.module.ModuleContainer; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; public class ModuleRegistry { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConcurrentHashMap moduleContainersByKey = new ConcurrentHashMap<>(); private final SpringComponentContainer parent; private final Function fileSystemProvider; public ModuleRegistry(SpringComponentContainer parent, Function fileSystemProvider) { this.parent = parent; this.fileSystemProvider = fileSystemProvider; } public ModuleContainer getContainerFor(String moduleKey) { return moduleContainersByKey.computeIfAbsent(moduleKey, key -> createContainer(key, fileSystemProvider.apply(key))); } @Nullable public ModuleContainer getContainerIfStarted(String moduleKey) { return moduleContainersByKey.get(moduleKey); } private ModuleContainer createContainer(Object moduleKey, @Nullable ClientModuleFileSystem clientFileSystem) { LOG.debug("Creating container for module '" + moduleKey + "'"); var moduleContainer = new ModuleContainer(parent, false); if (clientFileSystem != null) { moduleContainer.add(clientFileSystem); } moduleContainer.startComponents(); return moduleContainer; } public void unregisterModule(String moduleKey) { var container = moduleContainersByKey.remove(moduleKey); if (container != null) { container.stopComponents(); } } public void stopAll() { moduleContainersByKey.values().forEach(SpringComponentContainer::stopComponents); moduleContainersByKey.clear(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/global/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.global; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/module/DefaultModuleFileEvent.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.module; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent; public class DefaultModuleFileEvent implements ModuleFileEvent { private final InputFile target; private final ModuleFileEvent.Type type; private DefaultModuleFileEvent(InputFile target, Type type) { this.target = target; this.type = type; } public static DefaultModuleFileEvent of(InputFile target, Type type) { return new DefaultModuleFileEvent(target, type); } @Override public InputFile getTarget() { return target; } @Override public Type getType() { return type; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/module/ModuleContainer.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.module; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.container.ContainerLifespan; import org.sonarsource.sonarlint.core.analysis.container.analysis.AnalysisContainer; import org.sonarsource.sonarlint.core.analysis.container.analysis.IssueListenerHolder; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.LanguageDetection; import org.sonarsource.sonarlint.core.analysis.container.global.AnalysisExtensionInstaller; import org.sonarsource.sonarlint.core.analysis.sonarapi.ActiveRulesAdapter; import org.sonarsource.sonarlint.core.analysis.sonarapi.SonarLintModuleFileSystem; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; import static org.sonarsource.sonarlint.core.commons.tracing.Trace.startChild; public class ModuleContainer extends SpringComponentContainer { private final boolean isTransient; public ModuleContainer(SpringComponentContainer parent, boolean isTransient) { super(parent); this.isTransient = isTransient; } @Override protected void doBeforeStart() { add( SonarLintModuleFileSystem.class, ModuleInputFileBuilder.class, FileMetadata.class, LanguageDetection.class, ModuleFileEventNotifier.class); getParent().getComponentByType(AnalysisExtensionInstaller.class).install(this, ContainerLifespan.MODULE); } public boolean isTransient() { return isTransient; } public AnalysisResults analyze(AnalysisConfiguration configuration, Consumer issueListener, ProgressIndicator progressIndicator, @Nullable Trace trace) { var analysisContainer = startChild(trace, "newAnalysisContainer", "analyze", () -> new AnalysisContainer(this, progressIndicator)); analysisContainer.add(configuration); analysisContainer.add(new IssueListenerHolder(issueListener)); analysisContainer.add(startChild(trace, "newActiveRulesAdapter", "analyze", () -> new ActiveRulesAdapter(configuration.activeRules()))); var defaultAnalysisResult = new AnalysisResults(); analysisContainer.add(defaultAnalysisResult); if (trace != null) { analysisContainer.add(trace); } analysisContainer.execute(trace); return defaultAnalysisResult; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/module/ModuleFileEventNotifier.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.module; import java.util.List; import java.util.Optional; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileEvent; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileListener; public class ModuleFileEventNotifier { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final List listeners; private final ModuleInputFileBuilder inputFileBuilder; public ModuleFileEventNotifier(Optional> listeners, ModuleInputFileBuilder inputFileBuilder) { this.listeners = listeners.orElse(List.of()); this.inputFileBuilder = inputFileBuilder; } public void fireModuleFileEvent(ClientModuleFileEvent event) { ModuleFileEvent apiEvent = DefaultModuleFileEvent.of(inputFileBuilder.create(event.target()), event.type()); listeners.forEach(l -> tryFireModuleFileEvent(l, apiEvent)); } private static void tryFireModuleFileEvent(ModuleFileListener listener, ModuleFileEvent event) { try { listener.process(event); } catch (Exception e) { LOG.error("Error processing file event", e); } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/module/ModuleInputFileBuilder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.module; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import org.sonar.api.batch.fs.InputFile.Type; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.LanguageDetection; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class ModuleInputFileBuilder { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final LanguageDetection langDetection; private final FileMetadata fileMetadata; public ModuleInputFileBuilder(LanguageDetection langDetection, FileMetadata fileMetadata) { this.langDetection = langDetection; this.fileMetadata = fileMetadata; } public SonarLintInputFile create(ClientInputFile inputFile) { var defaultInputFile = new SonarLintInputFile(inputFile, f -> { LOG.debug("Initializing metadata of file {}", f.uri()); var charset = f.charset(); InputStream stream; try { stream = f.inputStream(); } catch (IOException e) { throw new IllegalStateException("Failed to open a stream on file: " + f.uri(), e); } return fileMetadata.readMetadata(stream, charset != null ? charset : Charset.defaultCharset(), f.uri(), null); }); defaultInputFile.setType(inputFile.isTest() ? Type.TEST : Type.MAIN); var fileLanguage = inputFile.language(); if (fileLanguage != null) { LOG.debug("Language of file \"{}\" is set to \"{}\"", inputFile.uri(), fileLanguage); defaultInputFile.setLanguage(fileLanguage); } else { defaultInputFile.setLanguage(langDetection.language(defaultInputFile)); } return defaultInputFile; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/module/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container.module; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/container/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.container; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/ActiveRulesAdapter.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.rule.RuleKey; public class ActiveRulesAdapter implements ActiveRules { private final Collection allActiveRules; private final Map> activeRulesByRepository = new HashMap<>(); private final Map> activeRulesByLanguage = new HashMap<>(); private final Map> activeRulesByRepositoryAndKey = new HashMap<>(); private final Map> activeRulesByRepositoryAndInternalKey = new HashMap<>(); public ActiveRulesAdapter(Collection activeRules) { allActiveRules = List.copyOf(activeRules); for (ActiveRule r : allActiveRules) { if (r.internalKey() != null) { activeRulesByRepositoryAndInternalKey.computeIfAbsent(r.ruleKey().repository(), x -> new HashMap<>()).put(r.internalKey(), r); } activeRulesByRepositoryAndKey.computeIfAbsent(r.ruleKey().repository(), x -> new HashMap<>()).put(r.ruleKey().rule(), r); activeRulesByRepository.computeIfAbsent(r.ruleKey().repository(), x -> new ArrayList<>()).add(r); activeRulesByLanguage.computeIfAbsent(r.language(), x -> new ArrayList<>()).add(r); } } @Override public ActiveRule find(RuleKey ruleKey) { return activeRulesByRepositoryAndKey.getOrDefault(ruleKey.repository(), Collections.emptyMap()) .get(ruleKey.rule()); } @Override public Collection findAll() { return allActiveRules; } @Override public Collection findByRepository(String repository) { return activeRulesByRepository.getOrDefault(repository, Collections.emptyList()); } @Override public Collection findByLanguage(String language) { return activeRulesByLanguage.getOrDefault(language, Collections.emptyList()); } @Override public ActiveRule findByInternalKey(String repository, String internalKey) { return activeRulesByRepositoryAndInternalKey.containsKey(repository) ? activeRulesByRepositoryAndInternalKey.get(repository).get(internalKey) : null; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultAnalysisError.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextPointer; import org.sonar.api.batch.sensor.error.AnalysisError; import org.sonar.api.batch.sensor.error.NewAnalysisError; import org.sonar.api.batch.sensor.internal.SensorStorage; import static java.util.Objects.requireNonNull; import static org.sonar.api.utils.Preconditions.checkArgument; import static org.sonar.api.utils.Preconditions.checkState; public class DefaultAnalysisError extends DefaultStorable implements NewAnalysisError, AnalysisError { private InputFile inputFile; private String message; private TextPointer location; public DefaultAnalysisError() { super(null); } public DefaultAnalysisError(SensorStorage storage) { super(storage); } @Override public InputFile inputFile() { return inputFile; } @Override public String message() { return message; } @Override public TextPointer location() { return location; } @Override public NewAnalysisError onFile(InputFile inputFile) { checkArgument(inputFile != null, "Cannot use a inputFile that is null"); checkState(this.inputFile == null, "onFile() already called"); this.inputFile = inputFile; return this; } @Override public NewAnalysisError message(String message) { this.message = message; return this; } @Override public NewAnalysisError at(TextPointer location) { checkState(this.location == null, "at() already called"); this.location = location; return this; } @Override protected void doSave() { requireNonNull(this.inputFile, "inputFile is mandatory on AnalysisError"); storage.store(this); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultFilterableIssue.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.rule.RuleKey; import org.sonar.api.scan.issue.filter.FilterableIssue; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextPointer; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextRange; public class DefaultFilterableIssue implements FilterableIssue { private final Issue rawIssue; private final InputComponent component; public DefaultFilterableIssue(Issue rawIssue, InputComponent component) { this.rawIssue = rawIssue; this.component = component; } @Override public String componentKey() { return component.key(); } @Override public RuleKey ruleKey() { return rawIssue.getRuleKey(); } @Override public String severity() { throw unsupported(); } @Override public String message() { throw unsupported(); } @Override public Integer line() { return rawIssue.getStartLine(); } @Override public String projectKey() { throw unsupported(); } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } private static UnsupportedOperationException unsupported() { return new UnsupportedOperationException("Not available for issues filters"); } @Override public Double gap() { throw unsupported(); } public InputComponent getComponent() { return component; } @Override public TextRange textRange() { var textRange = rawIssue.getTextRange(); if (textRange == null) { return null; } return new DefaultTextRange(new DefaultTextPointer(textRange.getStartLine(), textRange.getStartLineOffset()), new DefaultTextPointer(textRange.getEndLine(), textRange.getEndLineOffset())); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultFlow.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.batch.sensor.issue.Issue; import org.sonar.api.batch.sensor.issue.IssueLocation; import org.sonar.api.batch.sensor.issue.NewIssue; public class DefaultFlow implements Issue.Flow { private final List locations; private final String description; private final NewIssue.FlowType type; public DefaultFlow(List locations, @Nullable String description, NewIssue.FlowType type) { this.locations = locations; this.description = description; this.type = type; } @Override public List locations() { return locations; } @CheckForNull @Override public String description() { return description; } @Override public NewIssue.FlowType type() { return type; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSensorContext.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.io.InputStream; import java.io.Serializable; import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputModule; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.cache.ReadCache; import org.sonar.api.batch.sensor.cache.WriteCache; import org.sonar.api.batch.sensor.code.NewSignificantCode; import org.sonar.api.batch.sensor.coverage.NewCoverage; import org.sonar.api.batch.sensor.cpd.NewCpdTokens; import org.sonar.api.batch.sensor.error.NewAnalysisError; import org.sonar.api.batch.sensor.highlighting.NewHighlighting; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.batch.sensor.issue.NewExternalIssue; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.batch.sensor.measure.NewMeasure; import org.sonar.api.batch.sensor.rule.NewAdHocRule; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.config.Configuration; import org.sonar.api.config.Settings; import org.sonar.api.scanner.fs.InputProject; import org.sonar.api.utils.Version; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewCoverage; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewCpdTokens; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewHighlighting; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewMeasure; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewSignificantCode; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewSymbolTable; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; public class DefaultSensorContext implements SensorContext { private static final NoOpNewHighlighting NO_OP_NEW_HIGHLIGHTING = new NoOpNewHighlighting(); private static final NoOpNewSymbolTable NO_OP_NEW_SYMBOL_TABLE = new NoOpNewSymbolTable(); private static final NoOpNewCpdTokens NO_OP_NEW_CPD_TOKENS = new NoOpNewCpdTokens(); private static final NoOpNewCoverage NO_OP_NEW_COVERAGE = new NoOpNewCoverage(); private static final NoOpNewSignificantCode NO_OP_NEW_SIGNIFICANT_CODE = new NoOpNewSignificantCode(); private final Settings settings; private final FileSystem fs; private final ActiveRules activeRules; private final SensorStorage sensorStorage; private final SonarLintInputProject project; private final SonarRuntime sqRuntime; private final ProgressIndicator progressIndicator; private final Configuration config; public DefaultSensorContext(SonarLintInputProject project, Settings settings, Configuration config, FileSystem fs, ActiveRules activeRules, SensorStorage sensorStorage, SonarRuntime sqRuntime, ProgressIndicator progressIndicator) { this.project = project; this.settings = settings; this.config = config; this.fs = fs; this.activeRules = activeRules; this.sensorStorage = sensorStorage; this.sqRuntime = sqRuntime; this.progressIndicator = progressIndicator; } @Override public Settings settings() { return settings; } @Override public Configuration config() { return config; } @Override public FileSystem fileSystem() { return fs; } @Override public ActiveRules activeRules() { return activeRules; } @Override public NewMeasure newMeasure() { return new NoOpNewMeasure<>(); } @Override public NewIssue newIssue() { return new DefaultSonarLintIssue(project, fs.baseDir().toPath(), sensorStorage); } @Override public NewHighlighting newHighlighting() { return NO_OP_NEW_HIGHLIGHTING; } @Override public NewCoverage newCoverage() { return NO_OP_NEW_COVERAGE; } @Override public InputModule module() { return project; } @Override public InputProject project() { return project; } @Override public Version getSonarQubeVersion() { return sqRuntime.getApiVersion(); } @Override public SonarRuntime runtime() { return sqRuntime; } @Override public NewSymbolTable newSymbolTable() { return NO_OP_NEW_SYMBOL_TABLE; } @Override public NewCpdTokens newCpdTokens() { return NO_OP_NEW_CPD_TOKENS; } @Override public NewAnalysisError newAnalysisError() { return new DefaultAnalysisError(sensorStorage); } @Override public boolean isCancelled() { return progressIndicator.isCanceled(); } @Override public void addContextProperty(String key, String value) { // NO OP } @Override public void markForPublishing(InputFile inputFile) { // NO OP } @Override public void markAsUnchanged(InputFile inputFile) { // NO OP } @Override public NewExternalIssue newExternalIssue() { throw unsupported(); } @Override public NewSignificantCode newSignificantCode() { return NO_OP_NEW_SIGNIFICANT_CODE; } @Override public NewAdHocRule newAdHocRule() { throw unsupported(); } private static UnsupportedOperationException unsupported() { return new UnsupportedOperationException("Not supported in SonarLint"); } @Override public boolean canSkipUnchangedFiles() { return false; } @Override public boolean isCacheEnabled() { return false; } @Override public ReadCache previousCache() { throw unsupported(); } @Override public WriteCache nextCache() { throw unsupported(); } @Override public void addTelemetryProperty(String key, String value) { // PLUGINAPI-95 NO OP } @Override public void addAnalysisData(String s, String s1, InputStream inputStream) { // PLUGINAPI-117 analysis data storage // NO OP } @Override public boolean isFeatureAvailable(String s) { return false; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSensorDescriptor.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.Arrays; import java.util.Collection; import java.util.function.Predicate; import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.config.Configuration; public class DefaultSensorDescriptor implements SensorDescriptor { private String name; private String[] languages = new String[0]; private InputFile.Type type = null; private String[] ruleRepositories = new String[0]; private boolean global = false; private Predicate configurationPredicate; public String name() { return name; } public Collection languages() { return Arrays.asList(languages); } @Nullable public InputFile.Type type() { return type; } public Collection ruleRepositories() { return Arrays.asList(ruleRepositories); } public Predicate configurationPredicate() { return configurationPredicate; } public boolean isGlobal() { return global; } @Override public DefaultSensorDescriptor name(String name) { this.name = name; return this; } @Override public DefaultSensorDescriptor onlyOnLanguage(String languageKey) { return onlyOnLanguages(languageKey); } @Override public DefaultSensorDescriptor onlyOnLanguages(String... languageKeys) { this.languages = languageKeys; return this; } @Override public DefaultSensorDescriptor onlyOnFileType(InputFile.Type type) { this.type = type; return this; } @Override public DefaultSensorDescriptor createIssuesForRuleRepository(String... repositoryKey) { return createIssuesForRuleRepositories(repositoryKey); } @Override public DefaultSensorDescriptor createIssuesForRuleRepositories(String... repositoryKeys) { this.ruleRepositories = repositoryKeys; return this; } @Override public SensorDescriptor global() { this.global = true; return this; } @Override public DefaultSensorDescriptor onlyWhenConfiguration(Predicate configurationPredicate) { this.configurationPredicate = configurationPredicate; return this; } @Override public SensorDescriptor processesFilesIndependently() { // Not used by SonarLint return this; } @Override public SensorDescriptor processesHiddenFiles() { // Not used by SonarLint return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSonarLintIssue.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.api.batch.fs.InputDir; import org.sonar.api.batch.rule.Severity; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.batch.sensor.issue.Issue; import org.sonar.api.batch.sensor.issue.IssueLocation; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.batch.sensor.issue.NewIssueLocation; import org.sonar.api.batch.sensor.issue.fix.QuickFix; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.PathUtils; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.SensorQuickFix; import org.sonarsource.sonarlint.plugin.api.issue.NewQuickFix; import org.sonarsource.sonarlint.plugin.api.issue.NewSonarLintIssue; import static java.util.Objects.requireNonNull; public class DefaultSonarLintIssue extends DefaultStorable implements Issue, NewIssue, NewSonarLintIssue { private final SonarLintInputProject project; private final Path baseDir; protected DefaultSonarLintIssueLocation primaryLocation; protected List flows = new ArrayList<>(); private RuleKey ruleKey; private Severity overriddenSeverity; private final List quickFixes; private Optional ruleDescriptionContextKey = Optional.empty(); private final Map overriddenImpacts; private final List internalTags = new ArrayList<>(); public DefaultSonarLintIssue(SonarLintInputProject project, Path baseDir, @Nullable SensorStorage storage) { super(storage); this.project = project; this.baseDir = baseDir; this.quickFixes = new ArrayList<>(); this.overriddenImpacts = new EnumMap<>(SoftwareQuality.class); } @Override public NewIssueLocation newLocation() { return new DefaultSonarLintIssueLocation(); } @Override public NewIssue setRuleDescriptionContextKey(@Nullable String ruleDescriptionContextKey) { this.ruleDescriptionContextKey = Optional.ofNullable(ruleDescriptionContextKey); return this; } @Override public NewIssue setCodeVariants(@Nullable Iterable iterable) { // not implemented return this; } @Override public NewIssue addInternalTag(String tag) { internalTags.add(tag); return this; } @Override public NewIssue addInternalTags(Collection tags) { internalTags.addAll(tags); return this; } @Override public NewIssue setInternalTags(@Nullable Collection tags) { internalTags.clear(); if (tags != null) { addInternalTags(tags); } return this; } @Override public DefaultSonarLintIssue forRule(RuleKey ruleKey) { this.ruleKey = ruleKey; return this; } @Override public RuleKey ruleKey() { return this.ruleKey; } @Override public DefaultSonarLintIssue gap(@Nullable Double gap) { // Gap not used in SonarLint return this; } @Override public DefaultSonarLintIssue overrideSeverity(@Nullable Severity severity) { this.overriddenSeverity = severity; return this; } @Override public Severity overriddenSeverity() { return this.overriddenSeverity; } @Override public DefaultSonarLintIssue overrideImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) { overriddenImpacts.put(softwareQuality, severity); return this; } @Override public Map overridenImpacts() { return overriddenImpacts; } @Override public Double gap() { throw new UnsupportedOperationException("No gap in SonarLint"); } @Override public IssueLocation primaryLocation() { return primaryLocation; } @Override public List flows() { return this.flows; } @Override public DefaultSonarLintIssue at(NewIssueLocation primaryLocation) { this.primaryLocation = rewriteLocation((DefaultSonarLintIssueLocation) primaryLocation); return this; } @Override public NewIssue addLocation(NewIssueLocation secondaryLocation) { return addFlow(List.of(secondaryLocation)); } @Override public NewIssue addFlow(Iterable locations) { return addFlow(locations, FlowType.UNDEFINED, null); } @Override public NewIssue addFlow(Iterable flowLocations, FlowType flowType, @Nullable String flowDescription) { List flowAsList = new ArrayList<>(); for (NewIssueLocation issueLocation : flowLocations) { flowAsList.add(rewriteLocation((DefaultSonarLintIssueLocation) issueLocation)); } flows.add(new DefaultFlow(flowAsList, flowDescription, flowType)); return this; } private DefaultSonarLintIssueLocation rewriteLocation(DefaultSonarLintIssueLocation location) { var component = location.inputComponent(); Optional dirOrModulePath = Optional.empty(); if (component instanceof InputDir dirComponent) { dirOrModulePath = Optional.of(baseDir.relativize(dirComponent.path())); } if (dirOrModulePath.isPresent()) { var path = PathUtils.sanitize(dirOrModulePath.get().toString()); var fixedLocation = new DefaultSonarLintIssueLocation(); fixedLocation.on(project); var fullMessage = new StringBuilder(); if (!StringUtils.isEmpty(path)) { fullMessage.append("[").append(path).append("] "); } fullMessage.append(location.message()); fixedLocation.message(fullMessage.toString()); return fixedLocation; } else { return location; } } @Override public void doSave() { requireNonNull(this.ruleKey, "ruleKey is mandatory on issue"); storage.store(this); } @Override public SensorQuickFix newQuickFix() { return new SensorQuickFix(); } @Override public DefaultSonarLintIssue addQuickFix(NewQuickFix newQuickFix) { // legacy method from sonarlint-plugin-api, keep for backward compatibility and remove later quickFixes.add((QuickFix) newQuickFix); return this; } @Override public DefaultSonarLintIssue addQuickFix(org.sonar.api.batch.sensor.issue.fix.NewQuickFix newQuickFix) { quickFixes.add((QuickFix) newQuickFix); return this; } @Override public List quickFixes() { return Collections.unmodifiableList(quickFixes); } @CheckForNull @Override public List codeVariants() { return Collections.emptyList(); } @Override public List internalTags() { return Collections.unmodifiableList(internalTags); } @Override public NewIssue setQuickFixAvailable(boolean qfAvailable) { // not relevant in SonarLint return this; } @Override public boolean isQuickFixAvailable() { return !quickFixes.isEmpty(); } @Override public Optional ruleDescriptionContextKey() { return ruleDescriptionContextKey; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSonarLintIssueLocation.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.Collections; import java.util.List; import org.apache.commons.lang3.Strings; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.issue.IssueLocation; import org.sonar.api.batch.sensor.issue.MessageFormatting; import org.sonar.api.batch.sensor.issue.NewIssueLocation; import org.sonar.api.batch.sensor.issue.NewMessageFormatting; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewMessageFormatting; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.abbreviate; import static org.apache.commons.lang3.StringUtils.trimToEmpty; public class DefaultSonarLintIssueLocation implements NewIssueLocation, IssueLocation { private InputComponent component; private TextRange textRange; private String message; @Override public DefaultSonarLintIssueLocation on(InputComponent component) { requireNonNull(component, "Component can't be null"); this.component = component; return this; } @Override public DefaultSonarLintIssueLocation at(TextRange location) { this.textRange = location; return this; } @Override public DefaultSonarLintIssueLocation message(String message) { this.message = abbreviate(trimToEmpty(sanitizeNulls(message)), MESSAGE_MAX_SIZE); return this; } @Override public NewIssueLocation message(String message, List newMessageFormatting) { // ignore formatting for now return message(message); } @Override public NewMessageFormatting newMessageFormatting() { return new NoOpNewMessageFormatting(); } private static String sanitizeNulls(String message) { return Strings.CS.replace(message, "\u0000", "[NULL]"); } @Override public InputComponent inputComponent() { return this.component; } @Override public TextRange textRange() { return textRange; } @Override public String message() { return this.message; } @Override public List messageFormattings() { return Collections.emptyList(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultStorable.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import javax.annotation.Nullable; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.sonar.api.batch.sensor.internal.SensorStorage; import static java.util.Objects.requireNonNull; import static org.sonar.api.utils.Preconditions.checkState; abstract class DefaultStorable { protected final SensorStorage storage; private boolean saved = false; protected DefaultStorable(@Nullable SensorStorage storage) { this.storage = storage; } public final void save() { requireNonNull(this.storage, "No persister on this object"); checkState(!saved, "This object was already saved"); doSave(); this.saved = true; } protected abstract void doSave(); @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultTempFolder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import javax.annotation.Nullable; import org.apache.commons.io.FileUtils; import org.sonar.api.Startable; import org.sonar.api.utils.TempFolder; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class DefaultTempFolder implements TempFolder, Startable { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final File tempDir; private final boolean deleteOnExit; public DefaultTempFolder(File tempDir) { this(tempDir, false); } public DefaultTempFolder(File tempDir, boolean deleteOnExit) { this.tempDir = tempDir; this.deleteOnExit = deleteOnExit; } @Override public File newDir() { return createTempDir(tempDir.toPath()).toFile(); } private static Path createTempDir(Path baseDir) { try { return Files.createTempDirectory(baseDir, null); } catch (IOException e) { throw new IllegalStateException("Failed to create temp directory", e); } } @Override public File newDir(String name) { var dir = new File(tempDir, name); try { FileUtils.forceMkdir(dir); } catch (IOException e) { throw new IllegalStateException("Failed to create temp directory - " + dir, e); } return dir; } @Override public File newFile() { return newFile(null, null); } @Override public File newFile(@Nullable String prefix, @Nullable String suffix) { return createTempFile(tempDir.toPath(), prefix, suffix).toFile(); } private static Path createTempFile(Path baseDir, @Nullable String prefix, @Nullable String suffix) { try { return Files.createTempFile(baseDir, prefix, suffix); } catch (IOException e) { throw new IllegalStateException("Failed to create temp file", e); } } public void clean() { try { if (tempDir.exists()) { Files.walkFileTree(tempDir.toPath(), DeleteRecursivelyFileVisitor.INSTANCE); } } catch (IOException e) { LOG.error("Failed to delete temp folder", e); } } @Override public void start() { // Nothing } @Override public void stop() { if (deleteOnExit) { clean(); } } private static final class DeleteRecursivelyFileVisitor extends SimpleFileVisitor { public static final DeleteRecursivelyFileVisitor INSTANCE = new DeleteRecursivelyFileVisitor(); @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.deleteIfExists(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.deleteIfExists(dir); return FileVisitResult.CONTINUE; } } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/SonarLintModuleFileSystem.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.stream.Stream; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileSystem; import org.sonarsource.sonarlint.core.analysis.container.module.ModuleInputFileBuilder; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileSystem; public class SonarLintModuleFileSystem implements ModuleFileSystem { private final ClientModuleFileSystem clientFileSystem; private final ModuleInputFileBuilder inputFileBuilder; public SonarLintModuleFileSystem(ClientModuleFileSystem clientFileSystem, ModuleInputFileBuilder inputFileBuilder) { this.clientFileSystem = clientFileSystem; this.inputFileBuilder = inputFileBuilder; } @Override public Stream files(String suffix, InputFile.Type type) { return clientFileSystem.files(suffix, type) .map(inputFileBuilder::create); } @Override public Stream files() { return clientFileSystem.files() .map(inputFileBuilder::create); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpFileLinesContext.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.measures.FileLinesContext; public class NoOpFileLinesContext implements FileLinesContext { @Override public void setIntValue(String metricKey, int line, int value) { } @Override public void setStringValue(String metricKey, int line, String value) { } @Override public void save() { } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpFileLinesContextFactory.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.measures.FileLinesContext; import org.sonar.api.measures.FileLinesContextFactory; public class NoOpFileLinesContextFactory implements FileLinesContextFactory { @Override public FileLinesContext createFor(InputFile inputFile) { return new NoOpFileLinesContext(); } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewCoverage.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.coverage.NewCoverage; public class NoOpNewCoverage implements NewCoverage { @Override public NewCoverage onFile(InputFile inputFile) { // no op return this; } @Override public NewCoverage lineHits(int line, int hits) { // no op return this; } @Override public NewCoverage conditions(int line, int conditions, int coveredConditions) { // no op return this; } @Override public void save() { // no op } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewCpdTokens.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.cpd.NewCpdTokens; public class NoOpNewCpdTokens implements NewCpdTokens { @Override public void save() { // Do nothing } @Override public NoOpNewCpdTokens onFile(InputFile inputFile) { // Do nothing return this; } @Override public NoOpNewCpdTokens addToken(TextRange range, String image) { // Do nothing return this; } @Override public NoOpNewCpdTokens addToken(int startLine, int startLineOffset, int endLine, int endLineOffset, String image) { // Do nothing return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewHighlighting.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.highlighting.NewHighlighting; import org.sonar.api.batch.sensor.highlighting.TypeOfText; public class NoOpNewHighlighting implements NewHighlighting { @Override public void save() { // Do nothing } @Override public NoOpNewHighlighting onFile(InputFile inputFile) { // Do nothing return this; } @Override public NoOpNewHighlighting highlight(int startLine, int startLineOffset, int endLine, int endLineOffset, TypeOfText typeOfText) { // Do nothing return this; } @Override public NoOpNewHighlighting highlight(TextRange range, TypeOfText typeOfText) { // Do nothing return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewMeasure.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import java.io.Serializable; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.measure.Metric; import org.sonar.api.batch.sensor.measure.NewMeasure; public class NoOpNewMeasure implements NewMeasure { @Override public NoOpNewMeasure on(InputComponent component) { // do nothing return this; } @Override public NoOpNewMeasure forMetric(Metric metric) { // do nothing return this; } @Override public NoOpNewMeasure withValue(Serializable value) { // do nothing return this; } @Override public void save() { // do nothing } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewMessageFormatting.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.sensor.issue.MessageFormatting; import org.sonar.api.batch.sensor.issue.NewMessageFormatting; public class NoOpNewMessageFormatting implements NewMessageFormatting { @Override public NoOpNewMessageFormatting start(int start) { return this; } @Override public NoOpNewMessageFormatting end(int end) { return this; } @Override public NoOpNewMessageFormatting type(MessageFormatting.Type type) { return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewSignificantCode.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.code.NewSignificantCode; public class NoOpNewSignificantCode implements NewSignificantCode { @Override public void save() { // no op } @Override public NewSignificantCode onFile(InputFile file) { // no op return this; } @Override public NewSignificantCode addRange(TextRange range) { // no op return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewSymbolTable.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.sensor.symbol.NewSymbol; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; public class NoOpNewSymbolTable implements NewSymbolTable, NewSymbol { @Override public void save() { // Do nothing } @Override public NoOpNewSymbolTable onFile(InputFile inputFile) { // Do nothing return this; } @Override public NoOpNewSymbolTable newSymbol(int startLine, int startLineOffset, int endLine, int endLineOffset) { // Do nothing return this; } @Override public NoOpNewSymbolTable newSymbol(TextRange range) { // Do nothing return this; } @Override public NoOpNewSymbolTable newReference(int startLine, int startLineOffset, int endLine, int endLineOffset) { // Do nothing return this; } @Override public NoOpNewSymbolTable newReference(TextRange range) { // Do nothing return this; } } ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @javax.annotation.ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; ================================================ FILE: backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/sonarapi/package-info.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis.sonarapi; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/AnalysisQueueTest.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.Map; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand; import org.sonarsource.sonarlint.core.analysis.command.UnregisterModuleCommand; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class AnalysisQueueTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_prioritize_unregister_module_commands_over_analyses() throws InterruptedException { var analysisQueue = new AnalysisQueue(); var taskManager = mock(TaskManager.class); analysisQueue.post(new AnalyzeCommand("key", UUID.randomUUID(), null, null, null, null, new SonarLintCancelMonitor(), taskManager, null, () -> true, Set.of(), Map.of())); var unregisterModuleCommand = new UnregisterModuleCommand("key"); analysisQueue.post(unregisterModuleCommand); var command = analysisQueue.takeNextCommand(); assertThat(command).isEqualTo(unregisterModuleCommand); } @Test void it_should_not_queue_a_canceled_command() throws InterruptedException { var canceledProgressMonitor = new SonarLintCancelMonitor(); var progressMonitor = new SonarLintCancelMonitor(); var analysisQueue = new AnalysisQueue(); var taskManager = mock(TaskManager.class); var canceledCommand = new AnalyzeCommand("1", UUID.randomUUID(), TriggerType.FORCED, null, null, null, canceledProgressMonitor, taskManager, null, () -> true, Set.of(), Map.of()); var command = new AnalyzeCommand("2", UUID.randomUUID(), TriggerType.FORCED, null, null, null, progressMonitor, taskManager, null, () -> true, Set.of(), Map.of()); canceledProgressMonitor.cancel(); analysisQueue.post(canceledCommand); analysisQueue.post(command); var nextCommand = analysisQueue.takeNextCommand(); assertThat(nextCommand).isEqualTo(command); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisConfigurationTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import testutils.TestClientInputFile; import static java.nio.file.Files.createDirectory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class AnalysisConfigurationTests { @AfterEach void init_property() { System.clearProperty("sonarlint.debug.active.rules"); } @Test void testToString_and_getters(@TempDir Path temp) throws Exception { Map props = new HashMap<>(); props.put("sonar.java.libraries", "foo bar"); final var srcFile1 = createDirectory(temp.resolve("src1")); final var srcFile2 = createDirectory(temp.resolve("src2")); final var srcFile3 = createDirectory(temp.resolve("src3")); ClientInputFile inputFile = new TestClientInputFile(temp, srcFile1, false, StandardCharsets.UTF_8, null); ClientInputFile inputFileWithLanguage = new TestClientInputFile(temp, srcFile2, false, StandardCharsets.UTF_8, SonarLanguage.JAVA); ClientInputFile testInputFile = new TestClientInputFile(temp, srcFile3, true, null, SonarLanguage.PHP); var baseDir = createDirectory(temp.resolve("baseDir")); var activeRuleWithParams = newActiveRule("php:S123", Map.of("param1", "value1")); var config = AnalysisConfiguration.builder() .setBaseDir(baseDir) .addInputFile(inputFile) .addInputFiles(inputFileWithLanguage) .addInputFiles(List.of(testInputFile)) .putAllExtraProperties(props) .putExtraProperty("sonar.foo", "bar") .addActiveRules(List.of(newActiveRule("java:S123"), newActiveRule("java:S456"))) .addActiveRule(activeRuleWithParams) .addActiveRules(newActiveRule("python:S123"), newActiveRule("python:S456")) .build(); assertThat(config).hasToString("[\n" + " baseDir: " + baseDir + "\n" + " extraProperties: {sonar.java.libraries=foo bar, sonar.foo=bar}\n" + " activeRules: [2 python, 2 java, 1 php]\n" + " inputFiles: [\n" + " " + srcFile1.toUri() + " (UTF-8)\n" + " " + srcFile2.toUri() + " (UTF-8) [java]\n" + " " + srcFile3.toUri() + " (default) [test] [php]\n" + " ]\n" + "]\n"); assertThat(config.baseDir()).isEqualTo(baseDir); assertThat(config.inputFiles()).containsExactly(inputFile, inputFileWithLanguage, testInputFile); assertThat(config.extraProperties()).containsExactly(entry("sonar.java.libraries", "foo bar"), entry("sonar.foo", "bar")); assertThat(config.activeRules()).extracting(ActiveRule::ruleKey).map(RuleKey::toString).containsExactly("java:S123", "java:S456", "php:S123", "python:S123", "python:S456"); } @Test void testToString_and_getters_when_empty() { var config = AnalysisConfiguration.builder().build(); assertThat(config).hasToString(""" [ baseDir: null extraProperties: {} activeRules: [] inputFiles: [ ] ] """); assertThat(config.baseDir()).isNull(); assertThat(config.inputFiles()).isEmpty(); assertThat(config.activeRules()).isEmpty(); } @Test void testToString_and_getters_when_active_rules_verbose() { System.setProperty("sonarlint.debug.active.rules", "true"); var activeRuleWithParams = newActiveRule("php:S123", Map.of("param1", "value1")); var config = AnalysisConfiguration.builder() .addActiveRules(List.of(newActiveRule("java:S123"), newActiveRule("java:S456"))) .addActiveRules(activeRuleWithParams) .addActiveRules(newActiveRule("python:S123"), newActiveRule("python:S456")) .build(); assertThat(config).hasToString(""" [ baseDir: null extraProperties: {} activeRules: [java:S123, java:S456, php:S123{param1=value1}, python:S123, python:S456] inputFiles: [ ] ] """); assertThat(config.baseDir()).isNull(); assertThat(config.inputFiles()).isEmpty(); assertThat(config.activeRules()).extracting(ActiveRule::ruleKey).map(RuleKey::toString).containsExactly("java:S123", "java:S456", "php:S123", "python:S123", "python:S456"); } @Test void testToString_and_getters_when_active_rules_not_verbose() { var activeRuleWithParams = newActiveRule("php:S123", Map.of("param1", "value1")); var config = AnalysisConfiguration.builder() .addActiveRules(List.of(newActiveRule("java:S123"), newActiveRule("java:S456"))) .addActiveRules(activeRuleWithParams) .addActiveRules(newActiveRule("python:S123"), newActiveRule("python:S456")) .build(); assertThat(config).hasToString(""" [ baseDir: null extraProperties: {} activeRules: [2 python, 2 java, 1 php] inputFiles: [ ] ] """); assertThat(config.baseDir()).isNull(); assertThat(config.inputFiles()).isEmpty(); assertThat(config.activeRules()).extracting(ActiveRule::ruleKey).map(RuleKey::toString).containsExactly("java:S123", "java:S456", "php:S123", "python:S123", "python:S456"); } private static ActiveRule newActiveRule(String ruleKey) { return newActiveRule(ruleKey, Map.of()); } private static ActiveRule newActiveRule(String ruleKey, Map params) { return new ActiveRule() { @Override public RuleKey ruleKey() { return RuleKey.parse(ruleKey); } @Override public String severity() { return ""; } @Override public String language() { return ""; } @CheckForNull @Override public String param(String key) { return params().get(key); } @Override public Map params() { return params; } @CheckForNull @Override public String internalKey() { return ""; } @CheckForNull @Override public String templateRuleKey() { return ""; } @Override public String qpKey() { return ""; } @Override public String toString() { var sb = new StringBuilder(); sb.append(ruleKey); if (!params.isEmpty()) { sb.append(params); } return sb.toString(); } }; } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisSchedulerConfigurationTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.analysis.AnalysisScheduler; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.commons.PluginsLoader; import static java.nio.file.Files.createDirectory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class AnalysisSchedulerConfigurationTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void testDefaults() { var config = AnalysisSchedulerConfiguration.builder() .build(); assertThat(config.getWorkDir()).isNull(); assertThat(config.getEffectiveSettings()).isEmpty(); assertThat(config.getClientPid()).isZero(); } @Test void extraProps() { Map extraProperties = new HashMap<>(); extraProperties.put("foo", "bar"); var config = AnalysisSchedulerConfiguration.builder() .setExtraProperties(extraProperties) .build(); assertThat(config.getEffectiveSettings()).containsOnly(entry("foo", "bar")); } @Test void effectiveConfig_should_add_nodejs() { Map extraProperties = new HashMap<>(); extraProperties.put("foo", "bar"); var config = AnalysisSchedulerConfiguration.builder() .setExtraProperties(extraProperties) .setNodeJs(Paths.get("nodejsPath")) .build(); assertThat(config.getEffectiveSettings()).containsOnly(entry("foo", "bar"), entry("sonar.nodejs.executable", "nodejsPath")); } @Test void overrideDirs(@TempDir Path temp) throws Exception { var work = createDirectory(temp.resolve("work")); var config = AnalysisSchedulerConfiguration.builder() .setWorkDir(work) .build(); assertThat(config.getWorkDir()).isEqualTo(work); } @Test void providePid() { var config = AnalysisSchedulerConfiguration.builder().setClientPid(123).build(); assertThat(config.getClientPid()).isEqualTo(123); } @Test void should_not_fail_if_module_supplier_is_not_provided(@TempDir Path workDir) { assertDoesNotThrow(() -> { var analysisGlobalConfig = AnalysisSchedulerConfiguration.builder().setClientPid(1234L).setWorkDir(workDir).build(); var result = new PluginsLoader().load(new PluginsLoader.Configuration(Set.of(), Set.of(), false, Optional.empty()), Set.of()); new AnalysisScheduler(analysisGlobalConfig, result.getLoadedPlugins(), logTester.getLogOutput()); }); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; class ClientInputFileTests { @Test void testDefaults(@TempDir Path tempDir) { var path = tempDir.resolve("Foo.java"); var underTest = new ClientInputFile() { @Override public boolean isTest() { return false; } @Override public InputStream inputStream() { return null; } @Override public String getPath() { return path.toAbsolutePath().toString(); } @Override public String relativePath() { return path.getParent().toString(); } @Override public G getClientObject() { return null; } @Override public Charset getCharset() { return null; } @Override public String contents() { return null; } @Override public URI uri() { return path.toUri(); } }; assertThat(underTest.language()).isNull(); assertThat(underTest.uri()).hasScheme("file"); assertThat(underTest.uri().getPath()).endsWith("/Foo.java"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/DefaultLocationTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextPointer; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextRange; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class DefaultLocationTests { @Test void verify_accessors() { var inputFile = mock(ClientInputFile.class); var message = "fummy"; var sqApiTextRange = new DefaultTextRange(new DefaultTextPointer(1, 2), new DefaultTextPointer(3, 4)); var defaultLocation = new DefaultLocation(inputFile, sqApiTextRange, message); assertThat(defaultLocation.getInputFile()).isSameAs(inputFile); assertThat(defaultLocation.getMessage()).isSameAs(message); assertThat(defaultLocation.getTextRange().getStartLine()).isEqualTo(1); assertThat(defaultLocation.getTextRange().getStartLineOffset()).isEqualTo(2); assertThat(defaultLocation.getTextRange().getEndLine()).isEqualTo(3); assertThat(defaultLocation.getTextRange().getEndLineOffset()).isEqualTo(4); } @Test void text_range_can_be_null() { var inputFile = mock(ClientInputFile.class); var message = "fummy"; var defaultLocation = new DefaultLocation(inputFile, null, message); assertThat(defaultLocation.getTextRange()).isNull(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/IssueLocationTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.api; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.api.TextRange; import static org.assertj.core.api.Assertions.assertThat; class IssueLocationTests { @Test void it_should_return_text_range_details_if_provided() { var issueLocation = newIssueLocation(new TextRange(1, 2, 3, 4)); assertThat(issueLocation.getStartLine()).isEqualTo(1); assertThat(issueLocation.getStartLineOffset()).isEqualTo(2); assertThat(issueLocation.getEndLine()).isEqualTo(3); assertThat(issueLocation.getEndLineOffset()).isEqualTo(4); } @Test void it_should_return_null_details_if_no_text_range_provided() { var issueLocation = newIssueLocation(null); assertThat(issueLocation.getStartLine()).isNull(); assertThat(issueLocation.getStartLineOffset()).isNull(); assertThat(issueLocation.getEndLine()).isNull(); assertThat(issueLocation.getEndLineOffset()).isNull(); } private static IssueLocation newIssueLocation(@Nullable TextRange textRange) { return new IssueLocation() { @Override public ClientInputFile getInputFile() { return null; } @Override public TextRange getTextRange() { return textRange; } @Override public String getMessage() { return null; } }; } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/command/AnalyzeCommandTest.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.command; import java.net.URI; import java.util.Map; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import static org.assertj.core.api.Assertions.assertThat; class AnalyzeCommandTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_cancel_posting_command() throws Exception { var files = Set.of(new URI("file:///test1")); var props = Map.of("a", "b"); var cmd1 = newAnalyzeCommand(files, props); var cmd2 = newAnalyzeCommand(files, props); assertThat(cmd1.shouldCancelPost(cmd2)).isTrue(); } @Test void it_should_cancel_posting_command_if_canceled() throws Exception { var cmd1 = newAnalyzeCommand(Set.of(new URI("file:///test1")), Map.of()); var cmd2 = newAnalyzeCommand(Set.of(new URI("file:///test2")), Map.of()); cmd1.cancel(); assertThat(cmd1.shouldCancelPost(cmd2)).isTrue(); } @Test void it_should_not_cancel_when_files_are_different() throws Exception { var cmd1 = newAnalyzeCommand(Set.of(new URI("file:///test1")), Map.of()); var cmd2 = newAnalyzeCommand(Set.of(new URI("file:///test2")), Map.of()); assertThat(cmd1.shouldCancelPost(cmd2)).isFalse(); } @Test void if_should_cancel_task_in_queue_when_canceled() { var cmd = newAnalyzeCommand(Set.of(), Map.of()); cmd.cancel(); assertThat(cmd.shouldCancelQueue()).isTrue(); } @Test void it_should_not_cancel_task_in_queue_if_not_canceled() { var cmd = newAnalyzeCommand(Set.of(), Map.of()); assertThat(cmd.shouldCancelQueue()).isFalse(); } private static AnalyzeCommand newAnalyzeCommand(Set files, Map extraProps) { return new AnalyzeCommand( "moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> AnalysisConfiguration.builder().addInputFiles().build(), issue -> {}, null, new SonarLintCancelMonitor(), new TaskManager(), inputFiles -> {}, () -> true, files, extraProps ); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisSettingsTest.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import java.util.Collections; import java.util.Map; import org.junit.jupiter.api.Test; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.utils.System2; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.container.global.GlobalSettings; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class AnalysisSettingsTest { private final PropertyDefinitions propertyDefinitions = new PropertyDefinitions(System2.INSTANCE, Collections.emptyList()); private final GlobalSettings globalSettings = mock(GlobalSettings.class); @Test void trimAnalysisPropertyKeys() { AnalysisConfiguration analysisConfiguration = AnalysisConfiguration.builder() .putAllExtraProperties(Map.of("key1 ", "value1", "key1 ", "value11")).build(); AnalysisSettings analysisSettings = new AnalysisSettings(globalSettings, analysisConfiguration, propertyDefinitions); assertThat(analysisSettings.getProperties().keySet()) .contains("key1") .hasSize(1); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/AnalysisTempFolderProviderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class AnalysisTempFolderProviderTests { @Test void allMethodsShouldThrow() { var underTest = new AnalysisTempFolderProvider(); var tempFolder = underTest.provide(); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(tempFolder::newDir) .withMessage("Don't create temp folders during analysis"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> tempFolder.newDir("foo")) .withMessage("Don't create temp folders during analysis"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(tempFolder::newFile) .withMessage("Don't create temp files during analysis"); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> tempFolder.newFile("foo", "bar")) .withMessage("Don't create temp files during analysis"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/SonarLintPathPatternTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class SonarLintPathPatternTests { @Test void constructor_should_add_double_star_prefix_when_not_present() { assertThat(new SonarLintPathPattern("*.java")).hasToString("**/*.java"); } @Test void constructor_should_not_add_double_star_prefix_when_already_present() { assertThat(new SonarLintPathPattern("**/*.java")).hasToString("**/*.java"); } @Test void create_should_return_array_of_patterns() { var patterns = SonarLintPathPattern.create(new String[]{"*.java", "*.xml"}); assertThat(patterns).hasSize(2); assertThat(patterns[0].toString()).hasToString("**/*.java"); assertThat(patterns[1].toString()).hasToString("**/*.xml"); } @Test void create_should_return_empty_array_when_input_is_empty() { assertThat(SonarLintPathPattern.create(new String[]{})).isEmpty(); } @Test void match_should_match_java_files() { var pattern = new SonarLintPathPattern("*.java"); assertThat(pattern.match("src/main/java/Test.java")).isTrue(); assertThat(pattern.match("src/test/java/Test.java")).isTrue(); assertThat(pattern.match("Test.java")).isTrue(); assertThat(pattern.match("Test.txt")).isFalse(); } @Test void match_should_match_xml_files() { var pattern = new SonarLintPathPattern("*.xml"); assertThat(pattern.match("pom.xml")).isTrue(); assertThat(pattern.match("src/main/resources/config.xml")).isTrue(); assertThat(pattern.match("Test.java")).isFalse(); } @Test void match_should_match_with_path_patterns() { var pattern = new SonarLintPathPattern("src/**/*.java"); assertThat(pattern.match("src/main/java/Test.java")).isTrue(); assertThat(pattern.match("src/test/java/Test.java")).isTrue(); assertThat(pattern.match("Test.java")).isFalse(); } @Test void match_should_match_test_patterns() { var pattern = new SonarLintPathPattern("**/test/**/*.java"); assertThat(pattern.match("src/test/java/Test.java")).isTrue(); assertThat(pattern.match("src/main/java/Test.java")).isFalse(); } @Test void match_with_case_sensitive_should_respect_case() { var pattern = new SonarLintPathPattern("*.JAVA"); assertThat(pattern.match("src/main/java/Test.java", true)).isFalse(); assertThat(pattern.match("src/main/java/Test.JAVA", true)).isTrue(); } @Test void match_should_handle_different_path_separators() { var pattern = new SonarLintPathPattern("*.java"); assertThat(pattern.match("src\\main\\java\\Test.java")).isTrue(); assertThat(pattern.match("src/main/java/Test.java")).isTrue(); assertThat(pattern.match("src\\test\\java\\Test.java")).isTrue(); assertThat(pattern.match("Test.java")).isTrue(); } @Test void match_should_handle_path_without_extension() { var pattern = new SonarLintPathPattern("*.java"); var result = pattern.match("src/main/java/Test"); assertThat(result).isFalse(); } @Test void match_should_handle_path_with_dot_but_no_extension() { var pattern = new SonarLintPathPattern("*.java"); var result = pattern.match("src/main/java/Test."); assertThat(result).isFalse(); } @Test void toString_should_return_pattern_string() { var pattern = new SonarLintPathPattern("*.java"); var result = pattern.toString(); assertThat(result).isEqualTo("**/*.java"); } @Test void sanitizeExtension_should_handle_null() { assertThat(SonarLintPathPattern.sanitizeExtension(null)).isNull(); } @Test void sanitizeExtension_should_handle_empty_string() { assertThat(SonarLintPathPattern.sanitizeExtension("")).isEmpty(); } @Test void sanitizeExtension_should_remove_leading_dot() { assertThat(SonarLintPathPattern.sanitizeExtension(".java")).isEqualTo("java"); } @Test void sanitizeExtension_should_convert_to_lowercase() { assertThat(SonarLintPathPattern.sanitizeExtension("JAVA")).isEqualTo("java"); } @Test void sanitizeExtension_should_handle_extension_without_dot() { assertThat(SonarLintPathPattern.sanitizeExtension("java")).isEqualTo("java"); } @Test void sanitizeExtension_should_handle_mixed_case_with_dot() { assertThat(SonarLintPathPattern.sanitizeExtension(".JaVa")).isEqualTo("java"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/DefaultFilePredicatesTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FilePredicates; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Type; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; class DefaultFilePredicatesTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private InputFile javaFile; private FilePredicates predicates; @TempDir Path baseDir; @BeforeEach void before() throws IOException { predicates = new DefaultFilePredicates(); var filePath = baseDir.resolve("src/main/java/struts/Action.java"); Files.createDirectories(filePath.getParent()); Files.write(filePath, "foo".getBytes(StandardCharsets.UTF_8)); var clientInputFile = new OnDiskTestClientInputFile(filePath, "src/main/java/struts/Action.java", false, StandardCharsets.UTF_8, SonarLanguage.JAVA); InputStream fileInputStream = Files.newInputStream(filePath); javaFile = new SonarLintInputFile(clientInputFile, f -> new FileMetadata().readMetadata(fileInputStream, StandardCharsets.UTF_8, filePath.toUri(), null)) .setType(Type.MAIN) .setLanguage(SonarLanguage.JAVA); } @Test void all() { assertThat(predicates.all().apply(javaFile)).isTrue(); } @Test void none() { assertThat(predicates.none().apply(javaFile)).isFalse(); } @Test void matches_inclusion_pattern() { assertThat(predicates.matchesPathPattern("file:**/src/main/**/Action.java").apply(javaFile)).isTrue(); assertThat(predicates.matchesPathPattern("**/src/main/**/Action.java").apply(javaFile)).isTrue(); assertThat(predicates.matchesPathPattern("src/main/**/Action.java").apply(javaFile)).isTrue(); assertThat(predicates.matchesPathPattern("src/**/*.php").apply(javaFile)).isFalse(); } @Test void matches_inclusion_patterns() { assertThat(predicates.matchesPathPatterns(new String[] {"src/other/**.java", "src/main/**/Action.java"}).apply(javaFile)).isTrue(); assertThat(predicates.matchesPathPatterns(new String[] {}).apply(javaFile)).isTrue(); assertThat(predicates.matchesPathPatterns(new String[] {"src/other/**.java", "src/**/*.php"}).apply(javaFile)).isFalse(); } @Test void does_not_match_exclusion_pattern() { assertThat(predicates.doesNotMatchPathPattern("src/main/**/Action.java").apply(javaFile)).isFalse(); assertThat(predicates.doesNotMatchPathPattern("src/**/*.php").apply(javaFile)).isTrue(); } @Test void does_not_match_exclusion_patterns() { assertThat(predicates.doesNotMatchPathPatterns(new String[] {}).apply(javaFile)).isTrue(); assertThat(predicates.doesNotMatchPathPatterns(new String[] {"src/other/**.java", "src/**/*.php"}).apply(javaFile)).isTrue(); assertThat(predicates.doesNotMatchPathPatterns(new String[] {"src/other/**.java", "src/main/**/Action.java"}).apply(javaFile)).isFalse(); } @Test void has_relative_path_unsupported() { assertThrows(UnsupportedOperationException.class, () -> predicates.hasRelativePath("src/main/java/struts/Action.java").apply(javaFile)); } @Test void has_uri() { var uri = javaFile.uri(); assertThat(predicates.hasURI(uri).apply(javaFile)).isTrue(); assertThat(predicates.hasURI(baseDir.resolve("another.php").toUri()).apply(javaFile)).isFalse(); } @Test void has_name() { var fileName = javaFile.filename(); assertThat(predicates.hasFilename(fileName).apply(javaFile)).isTrue(); assertThat(predicates.hasFilename("another.php").apply(javaFile)).isFalse(); assertThat(predicates.hasFilename("Action.php").apply(javaFile)).isFalse(); } @Test void has_extension() { var extension = "java"; assertThat(predicates.hasExtension(extension).apply(javaFile)).isTrue(); assertThat(predicates.hasExtension("php").apply(javaFile)).isFalse(); assertThat(predicates.hasExtension("").apply(javaFile)).isFalse(); } @Test void has_path() { assertThrows(UnsupportedOperationException.class, () -> predicates.hasPath("src/main/java/struts/Action.java").apply(javaFile)); } @Test void is_file() { assertThat(predicates.is(javaFile.file()).apply(javaFile)).isTrue(); assertThat(predicates.is(new File("foo.php")).apply(javaFile)).isFalse(); } @Test void has_language() { assertThat(predicates.hasLanguage("java").apply(javaFile)).isTrue(); assertThat(predicates.hasLanguage("php").apply(javaFile)).isFalse(); } @Test void has_languages() { assertThat(predicates.hasLanguages(Arrays.asList("java", "php")).apply(javaFile)).isTrue(); assertThat(predicates.hasLanguages("java", "php").apply(javaFile)).isTrue(); assertThat(predicates.hasLanguages(Arrays.asList("cobol", "php")).apply(javaFile)).isFalse(); assertThat(predicates.hasLanguages("cobol", "php").apply(javaFile)).isFalse(); assertThat(predicates.hasLanguages(Collections.emptyList()).apply(javaFile)).isTrue(); } @Test void has_type() { assertThat(predicates.hasType(InputFile.Type.MAIN).apply(javaFile)).isTrue(); assertThat(predicates.hasType(InputFile.Type.TEST).apply(javaFile)).isFalse(); } @Test void has_status() { assertThat(predicates.hasAnyStatus().apply(javaFile)).isTrue(); try { predicates.hasStatus(InputFile.Status.SAME).apply(javaFile); fail("Expected exception"); } catch (Exception e) { assertThat(e).isInstanceOf(UnsupportedOperationException.class); } } @Test void not() { assertThat(predicates.not(predicates.hasType(InputFile.Type.MAIN)).apply(javaFile)).isFalse(); assertThat(predicates.not(predicates.hasType(InputFile.Type.TEST)).apply(javaFile)).isTrue(); } @Test void and() { // empty assertThat(predicates.and().apply(javaFile)).isTrue(); assertThat(predicates.and().apply(javaFile)).isTrue(); assertThat(predicates.and(Collections.emptyList()).apply(javaFile)).isTrue(); // two arguments assertThat(predicates.and(predicates.all(), predicates.all()).apply(javaFile)).isTrue(); assertThat(predicates.and(predicates.all(), predicates.none()).apply(javaFile)).isFalse(); assertThat(predicates.and(predicates.none(), predicates.all()).apply(javaFile)).isFalse(); // collection assertThat(predicates.and(Arrays.asList(predicates.all(), predicates.all())).apply(javaFile)).isTrue(); assertThat(predicates.and(Arrays.asList(predicates.all(), predicates.none())).apply(javaFile)).isFalse(); // array assertThat(predicates.and(new FilePredicate[] {predicates.all(), predicates.all()}).apply(javaFile)).isTrue(); assertThat(predicates.and(new FilePredicate[] {predicates.all(), predicates.none()}).apply(javaFile)).isFalse(); } @Test void or() { // empty assertThat(predicates.or().apply(javaFile)).isTrue(); assertThat(predicates.or().apply(javaFile)).isTrue(); assertThat(predicates.or(Collections.emptyList()).apply(javaFile)).isTrue(); // two arguments assertThat(predicates.or(predicates.all(), predicates.all()).apply(javaFile)).isTrue(); assertThat(predicates.or(predicates.all(), predicates.none()).apply(javaFile)).isTrue(); assertThat(predicates.or(predicates.none(), predicates.all()).apply(javaFile)).isTrue(); assertThat(predicates.or(predicates.none(), predicates.none()).apply(javaFile)).isFalse(); // collection assertThat(predicates.or(Arrays.asList(predicates.all(), predicates.all())).apply(javaFile)).isTrue(); assertThat(predicates.or(Arrays.asList(predicates.all(), predicates.none())).apply(javaFile)).isTrue(); assertThat(predicates.or(Arrays.asList(predicates.none(), predicates.none())).apply(javaFile)).isFalse(); // array assertThat(predicates.or(new FilePredicate[] {predicates.all(), predicates.all()}).apply(javaFile)).isTrue(); assertThat(predicates.or(new FilePredicate[] {predicates.all(), predicates.none()}).apply(javaFile)).isTrue(); assertThat(predicates.or(new FilePredicate[] {predicates.none(), predicates.none()}).apply(javaFile)).isFalse(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/InputFileBuilderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.FileUtils; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class InputFileBuilderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final LanguageDetection langDetection = mock(LanguageDetection.class); private final IssueExclusionsLoader issueExclusionsLoader = mock(IssueExclusionsLoader.class); private final FileMetadata metadata = new FileMetadata(); @TempDir private Path tempDir; @Test void testCreate() throws IOException { when(langDetection.language(any(InputFile.class))).thenReturn(SonarLanguage.JAVA); var path = tempDir.resolve("file"); Files.write(path, "test".getBytes(StandardCharsets.ISO_8859_1)); ClientInputFile file = new OnDiskTestClientInputFile(path, "file", true, StandardCharsets.ISO_8859_1); var builder = new InputFileBuilder(langDetection, metadata, issueExclusionsLoader); var inputFile = builder.create(file); assertThat(inputFile.type()).isEqualTo(InputFile.Type.TEST); assertThat(inputFile.file()).isEqualTo(path.toFile()); assertThat(inputFile.absolutePath()).isEqualTo(FileUtils.toSonarQubePath(path.toString())); assertThat(inputFile.language()).isEqualTo("java"); assertThat(inputFile.key()).isEqualTo(path.toUri().toString()); assertThat(inputFile.lines()).isEqualTo(1); verify(issueExclusionsLoader).createCharHandlerFor(inputFile); } @Test void testCreateWithLanguageSet() throws IOException { var path = tempDir.resolve("file"); Files.write(path, "test".getBytes(StandardCharsets.ISO_8859_1)); ClientInputFile file = new OnDiskTestClientInputFile(path, "file", true, StandardCharsets.ISO_8859_1, SonarLanguage.CPP); var builder = new InputFileBuilder(langDetection, metadata, issueExclusionsLoader); var inputFile = builder.create(file); assertThat(inputFile.language()).isEqualTo("cpp"); verifyNoInteractions(langDetection); } @Test void testCreate_lazy_error() throws IOException { when(langDetection.language(any(InputFile.class))).thenReturn(SonarLanguage.JAVA); ClientInputFile file = new OnDiskTestClientInputFile(Paths.get("INVALID"), "INVALID", true, StandardCharsets.ISO_8859_1); var builder = new InputFileBuilder(langDetection, metadata, issueExclusionsLoader); var slFile = builder.create(file); // Call any method that will trigger metadata initialization var thrown = assertThrows(IllegalStateException.class, () -> slFile.selectLine(1)); assertThat(thrown).hasMessageStartingWith("Failed to open a stream on file"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/InputFileCacheTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.batch.fs.InputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class InputFileCacheTests { private InputFileIndex cache; @BeforeEach void setUp() { cache = new InputFileIndex(); } @Test void testFiles() { var file1 = mock(InputFile.class); when(file1.filename()).thenReturn("file1.java"); when(file1.language()).thenReturn("lang1"); var file2 = mock(InputFile.class); when(file2.filename()).thenReturn("file2"); when(file2.language()).thenReturn("lang2"); cache.doAdd(file1); cache.doAdd(file2); assertThat(cache.inputFiles()).containsOnly(file1, file2); assertThrows(UnsupportedOperationException.class, () -> cache.inputFile("file1.java")); assertThat(cache.getFilesByExtension("java")).containsOnly(file1); assertThat(cache.getFilesByExtension("")).containsOnly(file2); assertThat(cache.getFilesByName("file1.java")).containsOnly(file1); assertThat(cache.languages()).containsExactly("lang1", "lang2"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/LanguageDetectionTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.nio.file.Path; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.resources.Language; import org.sonar.api.utils.MessageException; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.TestInputFileBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class LanguageDetectionTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir private Path basedir; @Test void test_sanitizeExtension() { assertThat(LanguageDetection.sanitizeExtension(".cbl")).isEqualTo("cbl"); assertThat(LanguageDetection.sanitizeExtension(".CBL")).isEqualTo("cbl"); assertThat(LanguageDetection.sanitizeExtension("CBL")).isEqualTo("cbl"); assertThat(LanguageDetection.sanitizeExtension("cbl")).isEqualTo("cbl"); } @Test void search_by_file_extension() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("Foo.java"))).isEqualTo(SonarLanguage.JAVA); assertThat(detection.language(newInputFile("src/Foo.java"))).isEqualTo(SonarLanguage.JAVA); assertThat(detection.language(newInputFile("Foo.JAVA"))).isEqualTo(SonarLanguage.JAVA); assertThat(detection.language(newInputFile("Foo.jav"))).isEqualTo(SonarLanguage.JAVA); assertThat(detection.language(newInputFile("Foo.Jav"))).isEqualTo(SonarLanguage.JAVA); assertThat(detection.language(newInputFile("abc.abap"))).isEqualTo(SonarLanguage.ABAP); assertThat(detection.language(newInputFile("abc.ABAP"))).isEqualTo(SonarLanguage.ABAP); assertThat(detection.language(newInputFile("abc.truc"))).isNull(); assertThat(detection.language(newInputFile("abap"))).isNull(); } @Test void recognise_yaml_files() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("lambda.yaml"))).isEqualTo(SonarLanguage.YAML); assertThat(detection.language(newInputFile("lambda.yml"))).isEqualTo(SonarLanguage.YAML); assertThat(detection.language(newInputFile("config/lambda.yml"))).isEqualTo(SonarLanguage.YAML); assertThat(detection.language(newInputFile("config/lambda.YAML"))).isEqualTo(SonarLanguage.YAML); assertThat(detection.language(newInputFile("wrong.ylm"))).isNull(); assertThat(detection.language(newInputFile("config.js"))).isNotEqualTo(SonarLanguage.YAML); } @Test void recognise_kts_files() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("settings.kts"))).isEqualTo(SonarLanguage.KOTLIN); assertThat(detection.language(newInputFile("settings.kms"))).isNull(); assertThat(detection.language(newInputFile("settings.js"))).isNotEqualTo(SonarLanguage.KOTLIN); } @Test void recognise_css_files() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("style.css"))).isEqualTo(SonarLanguage.CSS); assertThat(detection.language(newInputFile("style.less"))).isEqualTo(SonarLanguage.CSS); assertThat(detection.language(newInputFile("style.scss"))).isEqualTo(SonarLanguage.CSS); assertThat(detection.language(newInputFile("style.stylus"))).isNull(); } @Test void recognise_go_file() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("myFile.go"))).isEqualTo(SonarLanguage.GO); assertThat(detection.language(newInputFile("folder/myFile.go"))).isEqualTo(SonarLanguage.GO); assertThat(detection.language(newInputFile("style.nogo"))).isNull(); } @Test void recognise_terraform_file() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("myFile.tf"))).isEqualTo(SonarLanguage.TERRAFORM); assertThat(detection.language(newInputFile("folder/myFile.tf"))).isEqualTo(SonarLanguage.TERRAFORM); assertThat(detection.language(newInputFile("style.notf"))).isNull(); } @Test void should_not_fail_if_no_language() { var detection = new LanguageDetection(new MapSettings(Map.of()).asConfig()); assertThat(detection.language(newInputFile("Foo.blabla"))).isNull(); } @Test void fail_if_conflicting_language_suffix() { var settings = new MapSettings(Map.of(SonarLanguage.XML.getFileSuffixesPropKey(), "xhtml", SonarLanguage.HTML.getFileSuffixesPropKey(), "xhtml")); var detection = new LanguageDetection(settings.asConfig()); var inputFile = newInputFile("abc.xhtml"); var e = assertThrows(MessageException.class, () -> detection.language(inputFile)); assertThat(e.getMessage()) .contains("Language of file \"file://") .contains("abc.xhtml\" can not be decided as the file extension matches both ") .contains("HTML: xhtml") .contains("XML: xhtml"); } private InputFile newInputFile(String path) { return new TestInputFileBuilder(path).setBaseDir(basedir).build(); } static class MockLanguage implements Language { private final String key; private final String[] extensions; MockLanguage(String key, String... extensions) { this.key = key; this.extensions = extensions; } @Override public String getKey() { return key; } @Override public String getName() { return key; } @Override public String[] getFileSuffixes() { return extensions; } } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/ProgressReportTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class ProgressReportTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String THREAD_NAME = "progress"; @Test void die_on_stop() { var underTest = new ProgressReport(THREAD_NAME, 100); underTest.start("start"); assertThat(isThreadAlive(THREAD_NAME)).isTrue(); underTest.stop("stop"); assertThat(isThreadAlive(THREAD_NAME)).isFalse(); } @Test void accept_no_stop_msg() { var underTest = new ProgressReport(THREAD_NAME, 100); underTest.start("start"); assertThat(isThreadAlive(THREAD_NAME)).isTrue(); underTest.stop(null); assertThat(isThreadAlive(THREAD_NAME)).isFalse(); } @Test void do_not_block_app() { var underTest = new ProgressReport(THREAD_NAME, 100); underTest.start("start"); assertThat(isDaemon(THREAD_NAME)).isTrue(); underTest.stop("stop"); } @Test void do_log() { var underTest = new ProgressReport(THREAD_NAME, 100); underTest.start("start"); underTest.message(() -> "Some message"); await().atMost(5, SECONDS).untilAsserted(() -> assertThat(logTester.logs()).contains("start", "Some message")); underTest.stop("stop"); assertThat(logTester.logs()).contains("start", "Some message", "stop"); } private static boolean isDaemon(String name) { var t = getThread(name); return (t != null) && t.isDaemon(); } private static boolean isThreadAlive(String name) { var t = getThread(name); return (t != null) && t.isAlive(); } private static Thread getThread(String name) { var threads = Thread.getAllStackTraces().keySet(); for (Thread t : threads) { if (t.getName().equals(name)) { return t; } } return null; } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintFileSystemTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.File; import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.TestInputFileBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class SonarLintFileSystemTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private SonarLintFileSystem fs; @TempDir Path basedir; private final InputFileIndex inputFileCache = new InputFileIndex(); @BeforeEach void prepare() { fs = new SonarLintFileSystem(AnalysisConfiguration.builder().setBaseDir(basedir).build(), inputFileCache); } @Test void return_fake_workdir() { assertThat(fs.workDir()).isEqualTo(basedir.toFile()); } @Test void add_languages() { assertThat(fs.languages()).isEmpty(); inputFileCache.doAdd(new TestInputFileBuilder("src/Foo.php").setLanguage(SonarLanguage.PHP).build()); inputFileCache.doAdd(new TestInputFileBuilder("src/Bar.java").setLanguage(SonarLanguage.JAVA).build()); assertThat(fs.languages()).containsOnly("java", "php"); } @Test void files() { assertThat(fs.inputFiles(fs.predicates().all())).isEmpty(); var inputFile = new TestInputFileBuilder("src/Foo.php").setBaseDir(basedir).setLanguage(SonarLanguage.PHP).build(); inputFileCache.doAdd(inputFile); inputFileCache.doAdd(new TestInputFileBuilder("src/Bar.java").setBaseDir(basedir).setLanguage(SonarLanguage.JAVA).build()); inputFileCache.doAdd(new TestInputFileBuilder("src/Baz.java").setBaseDir(basedir).setLanguage(SonarLanguage.JAVA).build()); // no language inputFileCache.doAdd(new TestInputFileBuilder("src/readme.txt").setBaseDir(basedir).build()); // needed for CFamily assertThat(fs.inputFile(fs.predicates().is(inputFile.file()))).isNotNull(); assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir.toFile(), "src/Bar.java").toURI()))).isNotNull(); assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir.toFile(), "does/not/exist").toURI()))).isNull(); assertThat(fs.inputFile(fs.predicates().hasURI(new File(basedir.toFile(), "../src/Bar.java").toURI()))).isNull(); assertThat(fs.files(fs.predicates().all())).hasSize(4); assertThat(fs.files(fs.predicates().hasLanguage("java"))).hasSize(2); assertThat(fs.files(fs.predicates().hasLanguage("cobol"))).isEmpty(); assertThat(fs.hasFiles(fs.predicates().all())).isTrue(); assertThat(fs.hasFiles(fs.predicates().hasLanguage("java"))).isTrue(); assertThat(fs.hasFiles(fs.predicates().hasLanguage("cobol"))).isFalse(); assertThat(fs.inputFiles(fs.predicates().all())).hasSize(4); assertThat(fs.inputFiles(fs.predicates().hasLanguage("php"))).hasSize(1); assertThat(fs.inputFiles(fs.predicates().hasLanguage("java"))).hasSize(2); assertThat(fs.inputFiles(fs.predicates().hasLanguage("cobol"))).isEmpty(); assertThat(fs.languages()).containsOnly("java", "php"); } @Test void input_file_returns_null_if_file_not_found() { assertThat(fs.inputFile(fs.predicates().hasLanguage("cobol"))).isNull(); } @Test void input_file_fails_if_too_many_results() { inputFileCache.doAdd(new TestInputFileBuilder("src/Bar.java").setLanguage(SonarLanguage.JAVA).build()); inputFileCache.doAdd(new TestInputFileBuilder("src/Baz.java").setLanguage(SonarLanguage.JAVA).build()); var thrown = assertThrows(IllegalArgumentException.class, () -> fs.inputFile(fs.predicates().all())); assertThat(thrown).hasMessageStartingWith("expected one element"); } @Test void input_file_supports_non_indexed_predicates() { inputFileCache.doAdd(new TestInputFileBuilder("src/Bar.java").setLanguage(SonarLanguage.JAVA).build()); // it would fail if more than one java file assertThat(fs.inputFile(fs.predicates().hasLanguage("java"))).isNotNull(); } @Test void unsupported_resolve_path() { assertThrows(UnsupportedOperationException.class, () -> fs.resolvePath("foo")); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputDirTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.utils.PathUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class SonarLintInputDirTests { private SonarLintInputDir inputDir; private Path path; @BeforeEach void setUp() { path = Paths.get("file1").toAbsolutePath(); inputDir = new SonarLintInputDir(path); } @Test void testInputDir() { assertThat(inputDir.absolutePath()).isEqualTo(PathUtils.canonicalPath(path.toFile())); assertThat(inputDir.file()).isEqualTo(path.toFile()); assertThat(inputDir.key()).isEqualTo(PathUtils.canonicalPath(path.toFile())); assertThat(inputDir.isFile()).isFalse(); assertThat(inputDir.path()).isEqualTo(path); assertThat(inputDir.relativePath()).isEqualTo(inputDir.absolutePath()); assertThat(inputDir) .hasToString("[path=" + path + "]") .isNotEqualTo(mock(SonarLintInputDir.class)) .isEqualTo(inputDir); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFileTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile.Status; import testutils.FileUtils; import testutils.InMemoryTestClientInputFile; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class SonarLintInputFileTests { @Test void testGetters(@TempDir Path path) throws IOException { var filePath = path.resolve("foo.php"); Files.write(filePath, "test string".getBytes(StandardCharsets.UTF_8)); var inputFile = new OnDiskTestClientInputFile(filePath, "file", false, StandardCharsets.UTF_8); var fileInputStream = Files.newInputStream(filePath); var file = new SonarLintInputFile(inputFile, f -> new FileMetadata().readMetadata(fileInputStream, StandardCharsets.UTF_8, filePath.toUri(), null)); assertThat(file.contents()).isEqualTo("test string"); assertThat(file.md5Hash()).isEqualTo("6f8db599de986fab7a21625b7916589c"); assertThat(file.charset()).isEqualByComparingTo(StandardCharsets.UTF_8); assertThat(file.absolutePath()).isEqualTo(FileUtils.toSonarQubePath(inputFile.getPath())); assertThat(file.file()).isEqualTo(filePath.toFile()); assertThat(file.path()).isEqualTo(filePath); assertThat(file.getClientInputFile()).isEqualTo(inputFile); assertThat(file.status()).isEqualTo(Status.ADDED); assertThat(file) .isEqualTo(file) .isNotEqualTo(mock(SonarLintInputFile.class)); var stream = file.inputStream(); try (var reader = new BufferedReader(new InputStreamReader(stream))) { assertThat(reader.lines().collect(Collectors.joining())).isEqualTo("test string"); } } @Test void checkValidPointer() { var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null); var metadata = new FileMetadata.Metadata(2, new int[] {0, 10}, 16); var file = new SonarLintInputFile(inputFile, f -> metadata); assertThat(file.newPointer(1, 0).line()).isEqualTo(1); assertThat(file.newPointer(1, 0).lineOffset()).isZero(); // Don't fail file.newPointer(1, 9); file.newPointer(2, 0); file.newPointer(2, 5); } @Test void selectLine() { var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null); var metadata = new FileMetadata().readMetadata(new ByteArrayInputStream("bla bla a\nabcde\n\nabc".getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8, URI.create("file://foo.php"), null); var file = new SonarLintInputFile(inputFile, f -> metadata); assertThat(file.selectLine(1).start().line()).isEqualTo(1); assertThat(file.selectLine(1).start().lineOffset()).isZero(); assertThat(file.selectLine(1).end().line()).isEqualTo(1); assertThat(file.selectLine(1).end().lineOffset()).isEqualTo(9); // Don't fail when selecting empty line assertThat(file.selectLine(3).start().line()).isEqualTo(3); assertThat(file.selectLine(3).start().lineOffset()).isZero(); assertThat(file.selectLine(3).end().line()).isEqualTo(3); assertThat(file.selectLine(3).end().lineOffset()).isZero(); } @Test void testRangeOverlap() { var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null); var metadata = new FileMetadata.Metadata(2, new int[] {0, 10}, 16); var file = new SonarLintInputFile(inputFile, f -> metadata); // Don't fail assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)))).isTrue(); assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 2)))).isTrue(); assertThat(file.newRange(file.newPointer(1, 0), file.newPointer(1, 1)).overlap(file.newRange(file.newPointer(1, 1), file.newPointer(1, 2)))).isFalse(); assertThat(file.newRange(file.newPointer(1, 2), file.newPointer(1, 3)).overlap(file.newRange(file.newPointer(1, 0), file.newPointer(1, 2)))).isFalse(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/EnforceIssuesFilterTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.rule.RuleKey; import org.sonar.api.scan.issue.filter.IssueFilterChain; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.Metadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueInclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssuePattern; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultFilterableIssue; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class EnforceIssuesFilterTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private IssueInclusionPatternInitializer exclusionPatternInitializer; private EnforceIssuesFilter ignoreFilter; private DefaultFilterableIssue issue; private IssueFilterChain chain; @BeforeEach void init() { exclusionPatternInitializer = mock(IssueInclusionPatternInitializer.class); issue = mock(DefaultFilterableIssue.class); chain = mock(IssueFilterChain.class); when(chain.accept(issue)).thenReturn(true); } @Test void shouldPassToChainIfNoConfiguredPatterns() { ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer); assertThat(ignoreFilter.accept(issue, chain)).isTrue(); verify(chain).accept(issue); } @Test void shouldPassToChainIfRuleDoesNotMatch() { var rule = "rule"; var ruleKey = mock(RuleKey.class); when(ruleKey.toString()).thenReturn(rule); when(issue.ruleKey()).thenReturn(ruleKey); var matching = mock(IssuePattern.class); when(matching.matchRule(ruleKey)).thenReturn(false); when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(List.of(matching)); ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer); assertThat(ignoreFilter.accept(issue, chain)).isTrue(); verify(chain).accept(issue); } @Test void shouldAcceptIssueIfFullyMatched() { var rule = "rule"; var path = "org/sonar/api/Issue.java"; var ruleKey = mock(RuleKey.class); when(ruleKey.toString()).thenReturn(rule); when(issue.ruleKey()).thenReturn(ruleKey); var matching = mock(IssuePattern.class); when(matching.matchRule(ruleKey)).thenReturn(true); when(matching.matchFile(path)).thenReturn(true); when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(List.of(matching)); when(issue.getComponent()).thenReturn(createComponentWithPath(path)); ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer); assertThat(ignoreFilter.accept(issue, chain)).isTrue(); verifyNoInteractions(chain); } private InputComponent createComponentWithPath(String path) { return new SonarLintInputFile(new OnDiskTestClientInputFile(Paths.get(path), path, false, StandardCharsets.UTF_8), f -> mock(Metadata.class)); } @Test void shouldRefuseIssueIfRuleMatchesButNotPath() { var rule = "rule"; var path = "org/sonar/api/Issue.java"; var componentKey = "org.sonar.api.Issue"; var ruleKey = mock(RuleKey.class); when(ruleKey.toString()).thenReturn(rule); when(issue.ruleKey()).thenReturn(ruleKey); when(issue.componentKey()).thenReturn(componentKey); var matching = mock(IssuePattern.class); when(matching.matchRule(ruleKey)).thenReturn(true); when(matching.matchFile(path)).thenReturn(false); when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(List.of(matching)); when(issue.getComponent()).thenReturn(createComponentWithPath(path)); ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer); assertThat(ignoreFilter.accept(issue, chain)).isFalse(); verifyNoInteractions(chain); } @Test void shouldRefuseIssueIfRuleMatchesAndNotFile() { var rule = "rule"; var path = "org/sonar/api/Issue.java"; var ruleKey = mock(RuleKey.class); when(ruleKey.toString()).thenReturn(rule); when(issue.ruleKey()).thenReturn(ruleKey); var matching = mock(IssuePattern.class); when(matching.matchRule(ruleKey)).thenReturn(true); when(matching.matchFile(path)).thenReturn(true); when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(List.of(matching)); when(issue.getComponent()).thenReturn(new SonarLintInputProject()); ignoreFilter = new EnforceIssuesFilter(exclusionPatternInitializer); assertThat(ignoreFilter.accept(issue, chain)).isFalse(); verifyNoInteractions(chain); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/IgnoreIssuesFilterTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.rule.RuleKey; import org.sonar.api.scan.issue.filter.IssueFilterChain; import org.sonar.api.utils.WildcardPattern; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultFilterableIssue; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class IgnoreIssuesFilterTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final DefaultFilterableIssue issue = mock(DefaultFilterableIssue.class); private final IssueFilterChain chain = mock(IssueFilterChain.class); private final IgnoreIssuesFilter underTest = new IgnoreIssuesFilter(); private SonarLintInputFile component; private final RuleKey ruleKey = RuleKey.of("foo", "bar"); @BeforeEach void prepare() { component = mock(SonarLintInputFile.class); when(issue.getComponent()).thenReturn(component); when(issue.ruleKey()).thenReturn(ruleKey); } @Test void shouldPassToChainIfMatcherHasNoPatternForIssue() { when(chain.accept(issue)).thenReturn(true); assertThat(underTest.accept(issue, chain)).isTrue(); verify(chain).accept(any()); } @Test void shouldRejectIfRulePatternMatches() { var pattern = mock(WildcardPattern.class); when(pattern.match(ruleKey.toString())).thenReturn(true); underTest.addRuleExclusionPatternForComponent(component, pattern); assertThat(underTest.accept(issue, chain)).isFalse(); } @Test void shouldAcceptIfRulePatternDoesNotMatch() { var pattern = mock(WildcardPattern.class); when(pattern.match(ruleKey.toString())).thenReturn(false); underTest.addRuleExclusionPatternForComponent(component, pattern); assertThat(underTest.accept(issue, chain)).isFalse(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssueExclusionPatternInitializerTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class IssueExclusionPatternInitializerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void testNoConfiguration() { var patternsInitializer = new IssueExclusionPatternInitializer(new MapSettings(Map.of()).asConfig()); assertThat(patternsInitializer.hasConfiguredPatterns()).isFalse(); assertThat(patternsInitializer.getMulticriteriaPatterns()).isEmpty(); } @Test void shouldLogInvalidResourceKey() { Map settings = new HashMap<>(); settings.put("sonar.issue.ignore" + ".multicriteria", "1"); settings.put("sonar.issue.ignore" + ".multicriteria" + ".1." + "resourceKey", ""); settings.put("sonar.issue.ignore" + ".multicriteria" + ".1." + "ruleKey", "*"); new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(logTester.logs()).containsExactly("Issue exclusions are misconfigured. File pattern is mandatory for each entry of 'sonar.issue.ignore.multicriteria'"); } @Test void shouldLogInvalidRuleKey() { Map settings = new HashMap<>(); settings.put("sonar.issue.ignore" + ".multicriteria", "1"); settings.put("sonar.issue.ignore" + ".multicriteria" + ".1." + "resourceKey", "*"); settings.put("sonar.issue.ignore" + ".multicriteria" + ".1." + "ruleKey", ""); new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(logTester.logs()).containsExactly("Issue exclusions are misconfigured. Rule key pattern is mandatory for each entry of 'sonar.issue.ignore.multicriteria'"); } @Test void shouldReturnBlockPattern() { Map settings = new HashMap<>(); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY, "1,2,3"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionPatternInitializer.BEGIN_BLOCK_REGEXP, "// SONAR-OFF"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionPatternInitializer.END_BLOCK_REGEXP, "// SONAR-ON"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".2." + IssueExclusionPatternInitializer.BEGIN_BLOCK_REGEXP, "// FOO-OFF"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".2." + IssueExclusionPatternInitializer.END_BLOCK_REGEXP, "// FOO-ON"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".3." + IssueExclusionPatternInitializer.BEGIN_BLOCK_REGEXP, "// IGNORE-TO-EOF"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".3." + IssueExclusionPatternInitializer.END_BLOCK_REGEXP, ""); var patternsInitializer = new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue(); assertThat(patternsInitializer.hasFileContentPattern()).isTrue(); assertThat(patternsInitializer.hasMulticriteriaPatterns()).isFalse(); assertThat(patternsInitializer.getMulticriteriaPatterns()).isEmpty(); assertThat(patternsInitializer.getBlockPatterns()).hasSize(3); assertThat(patternsInitializer.getAllFilePatterns()).isEmpty(); } @Test void shouldLogInvalidStartBlockPattern() { Map settings = new HashMap<>(); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY, "1"); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionPatternInitializer.BEGIN_BLOCK_REGEXP, ""); settings.put(IssueExclusionPatternInitializer.PATTERNS_BLOCK_KEY + ".1." + IssueExclusionPatternInitializer.END_BLOCK_REGEXP, "// SONAR-ON"); new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(logTester.logs()).containsExactly("Issue exclusions are misconfigured. Start block regexp is mandatory for each entry of 'sonar.issue.ignore.block'"); } @Test void shouldReturnAllFilePattern() { Map settings = new HashMap<>(); settings.put(IssueExclusionPatternInitializer.PATTERNS_ALLFILE_KEY, "1,2"); settings.put(IssueExclusionPatternInitializer.PATTERNS_ALLFILE_KEY + ".1." + IssueExclusionPatternInitializer.FILE_REGEXP, "@SONAR-IGNORE-ALL"); settings.put(IssueExclusionPatternInitializer.PATTERNS_ALLFILE_KEY + ".2." + IssueExclusionPatternInitializer.FILE_REGEXP, "//FOO-IGNORE-ALL"); var patternsInitializer = new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue(); assertThat(patternsInitializer.hasFileContentPattern()).isTrue(); assertThat(patternsInitializer.hasMulticriteriaPatterns()).isFalse(); assertThat(patternsInitializer.getMulticriteriaPatterns()).isEmpty(); assertThat(patternsInitializer.getBlockPatterns()).isEmpty(); assertThat(patternsInitializer.getAllFilePatterns()).hasSize(2); } @Test void shouldLogInvalidAllFilePattern() { Map settings = new HashMap<>(); settings.put(IssueExclusionPatternInitializer.PATTERNS_ALLFILE_KEY, "1"); settings.put(IssueExclusionPatternInitializer.PATTERNS_ALLFILE_KEY + ".1." + IssueExclusionPatternInitializer.FILE_REGEXP, ""); new IssueExclusionPatternInitializer(new MapSettings(settings).asConfig()); assertThat(logTester.logs()).containsExactly("Issue exclusions are misconfigured. Remove blank entries from 'sonar.issue.ignore.allfile'"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssueInclusionPatternInitializerTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import static org.assertj.core.api.Assertions.assertThat; class IssueInclusionPatternInitializerTests { @Test void testNoConfiguration() { var patternsInitializer = new IssueInclusionPatternInitializer(new MapSettings(Collections.emptyMap()).asConfig()); patternsInitializer.initPatterns(); assertThat(patternsInitializer.hasConfiguredPatterns()).isFalse(); } @Test void shouldHavePatternsBasedOnMulticriteriaPattern() { Map settings = new HashMap<>(); settings.put("sonar.issue.enforce" + ".multicriteria", "1,2"); settings.put("sonar.issue.enforce" + ".multicriteria" + ".1." + "resourceKey", "org/foo/Bar.java"); settings.put("sonar.issue.enforce" + ".multicriteria" + ".1." + "ruleKey", "*"); settings.put("sonar.issue.enforce" + ".multicriteria" + ".2." + "resourceKey", "org/foo/Hello.java"); settings.put("sonar.issue.enforce" + ".multicriteria" + ".2." + "ruleKey", "checkstyle:MagicNumber"); var patternsInitializer = new IssueInclusionPatternInitializer(new MapSettings(settings).asConfig()); patternsInitializer.initPatterns(); assertThat(patternsInitializer.hasConfiguredPatterns()).isTrue(); assertThat(patternsInitializer.hasMulticriteriaPatterns()).isTrue(); assertThat(patternsInitializer.getMulticriteriaPatterns()).hasSize(2); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/pattern/IssuePatternTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern; import org.junit.jupiter.api.Test; import org.sonar.api.rules.Rule; import static org.assertj.core.api.Assertions.assertThat; class IssuePatternTests { @Test void shouldMatchJavaFile() { var javaFile = "org/foo/Bar.java"; assertThat(new IssuePattern("org/foo/Bar.java", "*").matchFile(javaFile)).isTrue(); assertThat(new IssuePattern("org/foo/*", "*").matchFile(javaFile)).isTrue(); assertThat(new IssuePattern("**Bar.java", "*").matchFile(javaFile)).isTrue(); assertThat(new IssuePattern("**", "*").matchFile(javaFile)).isTrue(); assertThat(new IssuePattern("org/*/?ar.java", "*").matchFile(javaFile)).isTrue(); assertThat(new IssuePattern("org/other/Hello.java", "*").matchFile(javaFile)).isFalse(); assertThat(new IssuePattern("org/foo/Hello.java", "*").matchFile(javaFile)).isFalse(); assertThat(new IssuePattern("org/*/??ar.java", "*").matchFile(javaFile)).isFalse(); assertThat(new IssuePattern("org/*/??ar.java", "*").matchFile(null)).isFalse(); assertThat(new IssuePattern("org/*/??ar.java", "*").matchFile("plop")).isFalse(); } @Test void shouldMatchRule() { var rule = Rule.create("checkstyle", "IllegalRegexp", "").ruleKey(); assertThat(new IssuePattern("*", "*").matchRule(rule)).isTrue(); assertThat(new IssuePattern("*", "checkstyle:*").matchRule(rule)).isTrue(); assertThat(new IssuePattern("*", "checkstyle:IllegalRegexp").matchRule(rule)).isTrue(); assertThat(new IssuePattern("*", "checkstyle:Illegal*").matchRule(rule)).isTrue(); assertThat(new IssuePattern("*", "*:*Illegal*").matchRule(rule)).isTrue(); assertThat(new IssuePattern("*", "pmd:IllegalRegexp").matchRule(rule)).isFalse(); assertThat(new IssuePattern("*", "pmd:*").matchRule(rule)).isFalse(); assertThat(new IssuePattern("*", "*:Foo*IllegalRegexp").matchRule(rule)).isFalse(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/IssueExclusionsLoaderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.Metadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.IgnoreIssuesFilter; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssueExclusionPatternInitializer; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.pattern.IssuePattern; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; class IssueExclusionsLoaderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private IssueExclusionPatternInitializer exclusionPatternInitializer; private IgnoreIssuesFilter ignoreIssuesFilter; private IssueExclusionsLoader scanner; @BeforeEach void before() { exclusionPatternInitializer = mock(IssueExclusionPatternInitializer.class); ignoreIssuesFilter = mock(IgnoreIssuesFilter.class); scanner = new IssueExclusionsLoader(exclusionPatternInitializer, ignoreIssuesFilter); } private SonarLintInputFile createFile(String path) { return new SonarLintInputFile(new OnDiskTestClientInputFile(Paths.get(path), path, false, StandardCharsets.UTF_8), f -> mock(Metadata.class)); } @Test void testToString() { assertThat(scanner).hasToString("Issues Exclusions - Source Scanner"); } @Test void createComputer() { assertThat(scanner.createCharHandlerFor(createFile("src/main/java/Foo.java"))).isNull(); when(exclusionPatternInitializer.getAllFilePatterns()).thenReturn(Collections.singletonList("pattern")); scanner = new IssueExclusionsLoader(exclusionPatternInitializer, ignoreIssuesFilter); assertThat(scanner.createCharHandlerFor(createFile("src/main/java/Foo.java"))).isNotNull(); } @Test void populateRuleExclusionPatterns() { var pattern1 = new IssuePattern("org/foo/Bar*.java", "*"); var pattern2 = new IssuePattern("org/foo/Hell?.java", "checkstyle:MagicNumber"); when(exclusionPatternInitializer.getMulticriteriaPatterns()).thenReturn(Arrays.asList(pattern1, pattern2)); var loader = new IssueExclusionsLoader(exclusionPatternInitializer, ignoreIssuesFilter); var file1 = createFile("org/foo/Bar.java"); loader.addMulticriteriaPatterns(file1); var file2 = createFile("org/foo/Baz.java"); loader.addMulticriteriaPatterns(file2); var file3 = createFile("org/foo/Hello.java"); loader.addMulticriteriaPatterns(file3); verify(ignoreIssuesFilter).addRuleExclusionPatternForComponent(file1, pattern1.getRulePattern()); verify(ignoreIssuesFilter).addRuleExclusionPatternForComponent(file3, pattern2.getRulePattern()); verifyNoMoreInteractions(ignoreIssuesFilter); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/IssueExclusionsRegexpScannerTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata.Metadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner.IssueExclusionsLoader.DoubleRegexpMatcher; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.OnDiskTestClientInputFile; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.mock; class IssueExclusionsRegexpScannerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private SonarLintInputFile javaFile; private List allFilePatterns; private List blockPatterns; private IssueExclusionsRegexpScanner regexpScanner; private final FileMetadata fileMetadata = new FileMetadata(); @BeforeEach void init() { blockPatterns = Arrays.asList(new DoubleRegexpMatcher(Pattern.compile("// SONAR-OFF"), Pattern.compile("// SONAR-ON")), new DoubleRegexpMatcher(Pattern.compile("// FOO-OFF"), Pattern.compile("// FOO-ON"))); allFilePatterns = Collections.singletonList(Pattern.compile("@SONAR-IGNORE-ALL")); javaFile = new SonarLintInputFile(new OnDiskTestClientInputFile(Paths.get("src/Foo.java"), "src/Foo.java", false, StandardCharsets.UTF_8), f -> mock(Metadata.class)); regexpScanner = new IssueExclusionsRegexpScanner(javaFile, allFilePatterns, blockPatterns); } @Test void shouldDetectPatternLastLine() throws URISyntaxException, IOException { var filePath = getResource("file-with-single-regexp-last-line.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isTrue(); } @Test void shouldDoNothing() throws Exception { var filePath = getResource("file-with-no-regexp.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isFalse(); } @Test void shouldExcludeAllIssues() throws Exception { var filePath = getResource("file-with-single-regexp.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isTrue(); } @Test void shouldExcludeAllIssuesEvenIfAlsoDoubleRegexps() throws Exception { var filePath = getResource("file-with-single-regexp-and-double-regexp.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isTrue(); } @Test void shouldExcludeLines() throws Exception { var filePath = getResource("file-with-double-regexp.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isFalse(); assertThat(IntStream.rangeClosed(1, 20).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(21, 25).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(26, 34).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); } @Test void shouldAddPatternToExcludeLinesTillTheEnd() throws Exception { var filePath = getResource("file-with-double-regexp-unfinished.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isFalse(); assertThat(IntStream.rangeClosed(1, 20).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(21, 34).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); } @Test void shouldAddPatternToExcludeSeveralLineRanges() throws Exception { var filePath = getResource("file-with-double-regexp-twice.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(javaFile.isIgnoreAllIssues()).isFalse(); assertThat(IntStream.rangeClosed(1, 20).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(21, 25).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(26, 28).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(29, 33).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); } @Test void shouldAddPatternToExcludeLinesWithWrongOrder() throws Exception { var filePath = getResource("file-with-double-regexp-wrong-order.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(IntStream.rangeClosed(1, 24).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(25, 35).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); } @Test void shouldAddPatternToExcludeLinesWithMess() throws Exception { var filePath = getResource("file-with-double-regexp-mess.txt"); fileMetadata.readMetadata(Files.newInputStream(filePath), UTF_8, filePath.toUri(), regexpScanner); assertThat(IntStream.rangeClosed(1, 20).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(21, 29).allMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); assertThat(IntStream.rangeClosed(30, 37).noneMatch(javaFile::isIgnoreAllIssuesOnLine)).isTrue(); } private Path getResource(String fileName) throws URISyntaxException { return Paths.get(this.getClass().getResource("/IssueExclusionsRegexpScannerTests/" + fileName).toURI()); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/issue/ignore/scanner/LineRangeTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.issue.ignore.scanner; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class LineRangeTests { @Test void lineRangeShouldBeOrdered() { assertThrows(IllegalArgumentException.class, () -> new LineRange(25, 12)); } @Test void shouldConvertLineRangeToLines() { var range = new LineRange(12, 15); assertThat(range.toLines()).containsOnly(12, 13, 14, 15); } @Test void shouldTestInclusionInRangeOfLines() { var range = new LineRange(12, 15); assertThat(range.in(3)).isFalse(); assertThat(range.in(12)).isTrue(); assertThat(range.in(13)).isTrue(); assertThat(range.in(14)).isTrue(); assertThat(range.in(15)).isTrue(); assertThat(range.in(16)).isFalse(); } @Test void testToString() { assertThat(new LineRange(12, 15)).hasToString("[12-15]"); } @Test void testEquals() { var range = new LineRange(12, 15); assertThat(range).isEqualTo(range) .isEqualTo(new LineRange(12, 15)) .isNotEqualTo(new LineRange(12, 2000)) .isNotEqualTo(new LineRange(1000, 2000)) .isNotEqualTo(null) .isNotEqualTo(new StringBuffer()); } @Test void testHashCode() { assertThat(new LineRange(12, 15)).hasSameHashCodeAs(new LineRange(12, 15).hashCode()); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SensorOptimizerTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.InputFileIndex; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintFileSystem; import org.sonarsource.sonarlint.core.analysis.sonarapi.ActiveRulesAdapter; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorDescriptor; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import testutils.TestInputFileBuilder; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class SensorOptimizerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private FileSystem fs; private SensorOptimizer optimizer; private MapSettings settings; private final InputFileIndex inputFileCache = new InputFileIndex(); @BeforeEach void prepare() { fs = new SonarLintFileSystem(mock(AnalysisConfiguration.class), inputFileCache); settings = new MapSettings(Map.of()); optimizer = new SensorOptimizer(fs, mock(ActiveRules.class), settings.asConfig()); } @Test void should_run_analyzer_with_no_metadata() { var descriptor = new DefaultSensorDescriptor(); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } @Test void should_optimize_on_language() { var descriptor = new DefaultSensorDescriptor() .onlyOnLanguages("java", "php"); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); inputFileCache.doAdd(new TestInputFileBuilder("src/Foo.java").setLanguage(SonarLanguage.JAVA).build()); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } @Test void should_optimize_on_type() { var descriptor = new DefaultSensorDescriptor() .onlyOnFileType(InputFile.Type.MAIN); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); inputFileCache.doAdd(new TestInputFileBuilder("tests/FooTest.java").setType(InputFile.Type.TEST).build()); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); inputFileCache.doAdd(new TestInputFileBuilder("src/Foo.java").setType(InputFile.Type.MAIN).build()); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } @Test void should_optimize_on_both_type_and_language() { var descriptor = new DefaultSensorDescriptor() .onlyOnLanguages("java", "php") .onlyOnFileType(InputFile.Type.MAIN); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); inputFileCache.doAdd(new TestInputFileBuilder("tests/FooTest.java").setLanguage(SonarLanguage.JAVA).setType(InputFile.Type.TEST).build()); inputFileCache.doAdd(new TestInputFileBuilder("src/Foo.cbl").setLanguage(SonarLanguage.COBOL).setType(InputFile.Type.MAIN).build()); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); inputFileCache.doAdd(new TestInputFileBuilder("src/Foo.java").setLanguage(SonarLanguage.JAVA).setType(InputFile.Type.MAIN).build()); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } @Test void should_optimize_on_repository() { var descriptor = new DefaultSensorDescriptor() .createIssuesForRuleRepositories("squid"); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); var ruleAnotherRepo = mock(ActiveRule.class); when(ruleAnotherRepo.ruleKey()).thenReturn(RuleKey.of("repo1", "foo")); ActiveRules activeRules = new ActiveRulesAdapter(List.of(ruleAnotherRepo)); optimizer = new SensorOptimizer(fs, activeRules, settings.asConfig()); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); var ruleSquid = mock(ActiveRule.class); when(ruleSquid.ruleKey()).thenReturn(RuleKey.of("squid", "rule")); activeRules = new ActiveRulesAdapter(asList(ruleSquid, ruleAnotherRepo)); optimizer = new SensorOptimizer(fs, activeRules, settings.asConfig()); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } @Test void should_optimize_on_settings() { var descriptor = new DefaultSensorDescriptor().onlyWhenConfiguration(c -> c.hasKey("sonar.foo.reportPath")); assertThat(optimizer.shouldExecute(descriptor)).isFalse(); settings = new MapSettings(Map.of("sonar.foo.reportPath", "foo")); optimizer = new SensorOptimizer(fs, mock(ActiveRules.class), settings.asConfig()); assertThat(optimizer.shouldExecute(descriptor)).isTrue(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SensorsExecutorTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.scanner.sensor.ProjectSensor; import org.sonarsource.sonarlint.core.analysis.sonarapi.DefaultSensorContext; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class SensorsExecutorTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); public static final DefaultSensorContext DEFAULT_SENSOR_CONTEXT = new DefaultSensorContext(null, null, null, null, null, null, null, new ProgressIndicator() { @Override public void notifyProgress(@Nullable String message, @Nullable Integer percentage) { // no-op } @Override public boolean isCanceled() { return false; } }); private static class MyClass { @Override public String toString() { return null; } } @Test void testDescribe() { Object withToString = new Object() { @Override public String toString() { return "desc"; } }; var withoutToString = new Object(); assertThat(SensorsExecutor.describe(withToString)).isEqualTo(("desc")); assertThat(SensorsExecutor.describe(withoutToString)).isEqualTo("java.lang.Object"); assertThat(SensorsExecutor.describe(new MyClass())).endsWith("MyClass"); } @Test void testThrowingSensorShouldBeLogged() { var sensorOptimizer = mock(SensorOptimizer.class); when(sensorOptimizer.shouldExecute(any())).thenReturn(true); var executor = new SensorsExecutor(DEFAULT_SENSOR_CONTEXT, sensorOptimizer, Optional.empty(), Optional.of(List.of(new ThrowingSensor()))); executor.execute(); assertThat(logTester.logs(LogOutput.Level.ERROR)).contains("Error executing sensor: 'Throwing sensor'"); } @Test void shouldRunGlobalSensorLast() { var sensorOptimizer = mock(SensorOptimizer.class); when(sensorOptimizer.shouldExecute(any())).thenReturn(true); var regularSensor = new RegularSensor(); var globalSensor = new GlobalSensor(); var oldGlobalSensor = new OldGlobalSensor(); var executor = new SensorsExecutor(DEFAULT_SENSOR_CONTEXT, sensorOptimizer, Optional.empty(), Optional.of(List.of(globalSensor, regularSensor, oldGlobalSensor))); executor.execute(); assertThat(logTester.logs(LogOutput.Level.INFO)).containsExactly("Executing 'Regular sensor'", "Executing 'Global sensor'", "Executing 'Old Global sensor'"); } private static class ThrowingSensor implements Sensor { @Override public void describe(SensorDescriptor descriptor) { descriptor.name("Throwing sensor"); } @Override public void execute(SensorContext context) { throw new Error(); } } private static class RegularSensor implements Sensor { @Override public void describe(SensorDescriptor descriptor) { descriptor.name("Regular sensor"); } @Override public void execute(SensorContext context) { SonarLintLogger.get().info("Executing 'Regular sensor'"); } } private static class GlobalSensor implements ProjectSensor { @Override public void describe(SensorDescriptor descriptor) { descriptor.name("Global sensor"); } @Override public void execute(SensorContext context) { SonarLintLogger.get().info("Executing 'Global sensor'"); } } private static class OldGlobalSensor implements Sensor { @Override public void describe(SensorDescriptor descriptor) { descriptor.name("Old Global sensor").global(); } @Override public void execute(SensorContext context) { SonarLintLogger.get().info("Executing 'Old Global sensor'"); } } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/sensor/SonarLintSensorStorageTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.analysis.sensor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.sensor.code.NewSignificantCode; import org.sonar.api.batch.sensor.coverage.NewCoverage; import org.sonar.api.batch.sensor.cpd.NewCpdTokens; import org.sonar.api.batch.sensor.error.AnalysisError; import org.sonar.api.batch.sensor.highlighting.NewHighlighting; import org.sonar.api.batch.sensor.issue.ExternalIssue; import org.sonar.api.batch.sensor.issue.Issue; import org.sonar.api.batch.sensor.measure.Measure; import org.sonar.api.batch.sensor.rule.AdHocRule; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonarsource.sonarlint.core.analysis.api.AnalysisResults; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.IssueListenerHolder; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.issue.IssueFilters; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class SonarLintSensorStorageTests { @Mock private ActiveRules activeRules; @Mock private IssueFilters filters; @Mock private IssueListenerHolder issueListener; @Mock private AnalysisResults analysisResult; @Mock private SonarLintInputFile inputFile; @Mock private ClientInputFile clientInputFile; private SonarLintSensorStorage underTest; @BeforeEach void setUp() { underTest = new SonarLintSensorStorage(activeRules, filters, issueListener, analysisResult); } @Test void store_Measure_doesnt_interact_with_its_param() { var measure = mock(Measure.class); underTest.store(measure); verifyNoInteractions(measure); } @Test void store_ExternalIssue_doesnt_interact_with_its_param() { var externalIssue = mock(ExternalIssue.class); underTest.store(externalIssue); verifyNoInteractions(externalIssue); } @Test void store_DefaultSignificantCode_doesnt_interact_with_its_param() { var significantCode = mock(NewSignificantCode.class); underTest.store(significantCode); verifyNoInteractions(significantCode); } @Test void store_DefaultHighlighting_doesnt_interact_with_its_param() { var highlighting = mock(NewHighlighting.class); underTest.store(highlighting); verifyNoInteractions(highlighting); } @Test void store_DefaultCoverage_doesnt_interact_with_its_param() { var coverage = mock(NewCoverage.class); underTest.store(coverage); verifyNoInteractions(coverage); } @Test void store_DefaultCpdTokens_doesnt_interact_with_its_param() { var cpdTokens = mock(NewCpdTokens.class); underTest.store(cpdTokens); verifyNoInteractions(cpdTokens); } @Test void store_DefaultSymbolTable_doesnt_interact_with_its_param() { var symbolTable = mock(NewSymbolTable.class); underTest.store(symbolTable); verifyNoInteractions(symbolTable); } @Test void store_AdHocRule_doesnt_interact_with_its_param() { var adHocRule = mock(AdHocRule.class); underTest.store(adHocRule); verifyNoInteractions(adHocRule); } @Test void store_should_throw_exception_for_non_sonarlint_issue() { var issue = mock(Issue.class); assertThatThrownBy(() -> underTest.store(issue)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Trying to store a non-SonarLint issue?"); } @Test void store_AnalysisError_should_add_failed_analysis_file() { var analysisError = mock(AnalysisError.class); when(analysisError.inputFile()).thenReturn(inputFile); when(inputFile.getClientInputFile()).thenReturn(clientInputFile); underTest.store(analysisError); verify(analysisResult).addFailedAnalysisFile(clientInputFile); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/global/AnalysisExtensionInstallerTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.Plugin; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.config.Configuration; import org.sonar.api.utils.Version; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.analysis.container.ContainerLifespan; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.SonarLintRuntimeImpl; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class AnalysisExtensionInstallerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String FAKE_PLUGIN_KEY = "foo"; private static final String JAVA_PLUGIN_KEY = "java"; private static final String DBD_PLUGIN_KEY = "dbd"; private static final Configuration EMPTY_CONFIG = new MapSettings(Map.of()).asConfig(); private static final Version PLUGIN_API_VERSION = Version.create(5, 4, 0); private static final long FAKE_PID = 123L; private static final SonarLintRuntime RUNTIME = new SonarLintRuntimeImpl(Version.create(8, 0), PLUGIN_API_VERSION, FAKE_PID); private AnalysisExtensionInstaller underTest; private LoadedPlugins loadedPlugins; private SpringComponentContainer container; @BeforeEach void prepare() { loadedPlugins = mock(LoadedPlugins.class); container = mock(SpringComponentContainer.class); underTest = new AnalysisExtensionInstaller(RUNTIME, loadedPlugins, EMPTY_CONFIG); } @Test void install_sonarlintside_extensions_with_default_lifespan_in_analysis_container_for_compatible_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin())); underTest.install(container, ContainerLifespan.ANALYSIS); verify(container).addExtension(FAKE_PLUGIN_KEY, FakeSonarLintDefaultLifespanComponent.class); } @Test void install_sonarlintside_extensions_with_single_analysis_lifespan_in_analysis_container_for_compatible_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintSingleAnalysisLifespanComponent.class))); underTest.install(container, ContainerLifespan.ANALYSIS); verify(container).addExtension(FAKE_PLUGIN_KEY, FakeSonarLintSingleAnalysisLifespanComponent.class); } @Test void install_sonarlintside_extensions_with_multiple_analysis_lifespan_in_global_container_for_compatible_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintMultipleAnalysisLifespanComponent.class))); underTest.install(container, ContainerLifespan.INSTANCE); verify(container).addExtension(FAKE_PLUGIN_KEY, FakeSonarLintMultipleAnalysisLifespanComponent.class); } @Test void install_sonarlintside_extensions_with_instance_lifespan_in_global_container_for_compatible_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintInstanceLifespanComponent.class))); underTest.install(container, ContainerLifespan.INSTANCE); verify(container).addExtension(FAKE_PLUGIN_KEY, FakeSonarLintInstanceLifespanComponent.class); } @Test void dont_install_sonarlintside_extensions_with_multiple_analysis_lifespan_in_analysis_container_for_compatible_plugins() { when(loadedPlugins.getAllPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintMultipleAnalysisLifespanComponent.class))); underTest.install(container, ContainerLifespan.ANALYSIS); verifyNoInteractions(container); } @Test void dont_install_sonarlintside_extensions_with_single_analysis_lifespan_in_global_container_for_compatible_plugins() { when(loadedPlugins.getAllPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintSingleAnalysisLifespanComponent.class))); underTest.install(container, ContainerLifespan.INSTANCE); verifyNoInteractions(container); } @Test void install_sonarlintside_extensions_with_module_lifespan_in_module_container_for_compatible_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin(FakeSonarLintModuleLifespanComponent.class))); underTest.install(container, ContainerLifespan.MODULE); verify(container).addExtension(FAKE_PLUGIN_KEY, FakeSonarLintModuleLifespanComponent.class); } @Test void install_sensors_for_sonarsource_plugins_by_language() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(JAVA_PLUGIN_KEY, new FakePlugin())); underTest.install(container, ContainerLifespan.ANALYSIS); verify(container).addExtension(JAVA_PLUGIN_KEY, FakeSensor.class); } @Test void install_sensors_for_sonarsource_plugins_by_allowlist() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(DBD_PLUGIN_KEY, new FakePlugin())); when(loadedPlugins.getAdditionalAllowedPlugins()).thenReturn(Set.of(DBD_PLUGIN_KEY)); underTest.install(container, ContainerLifespan.ANALYSIS); verify(container).addExtension(DBD_PLUGIN_KEY, FakeSensor.class); } @Test void dont_install_sensors_for_non_sonarsource_plugins() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new FakePlugin())); underTest.install(container, ContainerLifespan.ANALYSIS); verify(container, never()).addExtension(FAKE_PLUGIN_KEY, FakeSensor.class); } @Test void provide_sonarlint_context_for_plugin_definition() { var pluginInstance = new PluginStoringSonarLintPluginApiVersion(); when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, pluginInstance)); underTest = new AnalysisExtensionInstaller(RUNTIME, loadedPlugins, EMPTY_CONFIG); underTest.install(container, ContainerLifespan.ANALYSIS); assertThat(pluginInstance.sonarLintPluginApiVersion).isEqualTo(PLUGIN_API_VERSION); assertThat(pluginInstance.clientPid).isEqualTo(FAKE_PID); } @Test void log_when_plugin_throws() { when(loadedPlugins.getAnalysisPluginInstancesByKeys()).thenReturn(Map.of(FAKE_PLUGIN_KEY, new ThrowingPlugin())); underTest = new AnalysisExtensionInstaller(RUNTIME, loadedPlugins, EMPTY_CONFIG); underTest.install(container, ContainerLifespan.ANALYSIS); assertThat(logTester.logs(LogOutput.Level.ERROR)).contains("Error loading components for plugin 'foo'"); } private static class FakePlugin implements Plugin { private final Object component; private FakePlugin() { this(FakeSonarLintDefaultLifespanComponent.class); } public FakePlugin(Object component) { this.component = component; } @Override public void define(Context context) { context.addExtension(component); context.addExtension(FakeSensor.class); } } private static class ThrowingPlugin implements Plugin { @Override public void define(Context context) { throw new Error(); } } private static class PluginStoringSonarLintPluginApiVersion implements Plugin { Version sonarLintPluginApiVersion; long clientPid; @Override public void define(Context context) { if (context.getRuntime() instanceof SonarLintRuntime) { sonarLintPluginApiVersion = ((SonarLintRuntime) context.getRuntime()).getSonarLintPluginApiVersion(); clientPid = ((SonarLintRuntime) context.getRuntime()).getClientPid(); } } } @SonarLintSide private static class FakeSonarLintDefaultLifespanComponent { } @SonarLintSide(lifespan = SonarLintSide.SINGLE_ANALYSIS) private static class FakeSonarLintSingleAnalysisLifespanComponent { } @SonarLintSide(lifespan = SonarLintSide.MULTIPLE_ANALYSES) private static class FakeSonarLintMultipleAnalysisLifespanComponent { } @SonarLintSide(lifespan = "MODULE") private static class FakeSonarLintModuleLifespanComponent { } @SonarLintSide(lifespan = "INSTANCE") private static class FakeSonarLintInstanceLifespanComponent { } private static class FakeSensor implements Sensor { @Override public void describe(SensorDescriptor descriptor) { } @Override public void execute(SensorContext context) { } } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalSettingsTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.utils.System2; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class GlobalSettingsTests { @RegisterExtension SonarLintLogTester logTester = new SonarLintLogTester(); @Test void emptyNodePathPropertyForSonarJS() { var underTest = new GlobalSettings(AnalysisSchedulerConfiguration.builder().build(), new PropertyDefinitions(System2.INSTANCE)); var nodeJsExecutableValue = underTest.getString("sonar.nodejs.executable"); assertThat(nodeJsExecutableValue).isNull(); } @Test void customNodePathPropertyForSonarJS() { var providedNodePath = Paths.get("foo/bar/node"); var underTest = new GlobalSettings(AnalysisSchedulerConfiguration.builder().setNodeJs(providedNodePath).build(), new PropertyDefinitions(System2.INSTANCE)); var nodeJsExecutableValue = underTest.getString("sonar.nodejs.executable"); assertThat(nodeJsExecutableValue).isEqualTo(providedNodePath.toString()); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/global/GlobalTempFolderProviderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.global; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.utils.TempFolder; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import static org.assertj.core.api.Assertions.assertThat; class GlobalTempFolderProviderTests { @TempDir private Path workingDir; private final GlobalTempFolderProvider tempFolderProvider = new GlobalTempFolderProvider(); @Test void createTempFolderProps() throws Exception { TempFolder tempFolder = tempFolderProvider.provide(AnalysisSchedulerConfiguration.builder().setWorkDir(workingDir).build()); tempFolder.newDir(); tempFolder.newFile(); assertThat(getCreatedTempDir(workingDir)).exists(); assertThat(getCreatedTempDir(workingDir).list()).hasSize(2); FileUtils.deleteQuietly(workingDir.toFile()); } @Test @Disabled("SLCORE-821") void cleanUpOld() throws IOException { var creationTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(100); for (var i = 0; i < 3; i++) { var tmp = new File(workingDir.toFile(), ".sonarlinttmp_" + i); tmp.mkdirs(); setFileCreationDate(tmp, creationTime); } tempFolderProvider.provide(AnalysisSchedulerConfiguration.builder().setWorkDir(workingDir).build()); // this also checks that all other temps were deleted assertThat(getCreatedTempDir(workingDir)).exists(); FileUtils.deleteQuietly(workingDir.toFile()); } private File getCreatedTempDir(Path workingDir) { assertThat(workingDir).isDirectory(); assertThat(workingDir.toFile().listFiles()).hasSize(1); return workingDir.toFile().listFiles()[0]; } private void setFileCreationDate(File f, long time) throws IOException { var attributes = Files.getFileAttributeView(f.toPath(), BasicFileAttributeView.class); var creationTime = FileTime.fromMillis(time); attributes.setTimes(creationTime, creationTime, creationTime); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/module/ModuleInputFileBuilderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.container.module; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.LanguageDetection; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import testutils.FileUtils; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class ModuleInputFileBuilderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final LanguageDetection langDetection = mock(LanguageDetection.class); private final FileMetadata metadata = new FileMetadata(); @TempDir private Path tempDir; @Test void testCreate() throws IOException { when(langDetection.language(any(InputFile.class))).thenReturn(SonarLanguage.JAVA); var path = tempDir.resolve("file"); Files.write(path, "test".getBytes(StandardCharsets.ISO_8859_1)); ClientInputFile file = new OnDiskTestClientInputFile(path, "file", true, StandardCharsets.ISO_8859_1); var builder = new ModuleInputFileBuilder(langDetection, metadata); var inputFile = builder.create(file); assertThat(inputFile.type()).isEqualTo(InputFile.Type.TEST); assertThat(inputFile.file()).isEqualTo(path.toFile()); assertThat(inputFile.absolutePath()).isEqualTo(FileUtils.toSonarQubePath(path.toString())); assertThat(inputFile.language()).isEqualTo("java"); assertThat(inputFile.key()).isEqualTo(path.toUri().toString()); assertThat(inputFile.lines()).isEqualTo(1); } @Test void testCreateWithLanguageSet() throws IOException { var path = tempDir.resolve("file"); Files.write(path, "test".getBytes(StandardCharsets.ISO_8859_1)); ClientInputFile file = new OnDiskTestClientInputFile(path, "file", true, StandardCharsets.ISO_8859_1, SonarLanguage.CPP); var builder = new ModuleInputFileBuilder(langDetection, metadata); var inputFile = builder.create(file); assertThat(inputFile.language()).isEqualTo("cpp"); verifyNoInteractions(langDetection); } @Test void testCreate_lazy_error() throws IOException { when(langDetection.language(any(InputFile.class))).thenReturn(SonarLanguage.JAVA); ClientInputFile file = new OnDiskTestClientInputFile(Paths.get("INVALID"), "INVALID", true, StandardCharsets.ISO_8859_1); var builder = new ModuleInputFileBuilder(langDetection, metadata); var slFile = builder.create(file); // Call any method that will trigger metadata initialization var thrown = assertThrows(IllegalStateException.class, () -> slFile.selectLine(1)); assertThat(thrown).hasMessageStartingWith("Failed to open a stream on file"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/mediumtests/AnalysisSchedulerMediumTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.mediumtests; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; import javax.annotation.CheckForNull; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.analysis.AnalysisScheduler; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileSystem; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand; import org.sonarsource.sonarlint.core.commons.LogTestStartAndEnd; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.plugin.commons.PluginsLoader; import testutils.OnDiskTestClientInputFile; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; @ExtendWith(LogTestStartAndEnd.class) class AnalysisSchedulerMediumTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); private static final Consumer> NO_OP_ANALYSIS_STARTED_CONSUMER = inputFiles -> { }; private static final Supplier ANALYSIS_READY_SUPPLIER = () -> true; private static final Consumer NO_OP_ISSUE_LISTENER = issue -> { }; public static final TaskManager TASK_MANAGER = new TaskManager(); private AnalysisScheduler analysisScheduler; private volatile boolean engineStopped = true; private final SonarLintCancelMonitor progressMonitor = new SonarLintCancelMonitor(); @BeforeEach void prepare(@TempDir Path workDir) throws IOException { var enabledLanguages = Set.of(SonarLanguage.PYTHON); var analysisGlobalConfig = AnalysisSchedulerConfiguration.builder() .setClientPid(1234L) .setWorkDir(workDir) .setFileSystemProvider(this::provideFileSystem) .build(); var result = new PluginsLoader().load(new PluginsLoader.Configuration(Set.of(findPythonJarPath()), enabledLanguages, false, Optional.empty()), Set.of()); this.analysisScheduler = new AnalysisScheduler(analysisGlobalConfig, result.getLoadedPlugins(), logTester.getLogOutput()); engineStopped = false; } private ClientModuleFileSystem provideFileSystem(String moduleKey) { return aModuleFileSystem(); } @AfterEach void cleanUp() { if (!engineStopped) { this.analysisScheduler.stop(); } } @Test void should_analyze_a_file_inside_a_module(@TempDir Path baseDir) throws Exception { var content = """ def foo(): x = 9; # trailing comment """; ClientInputFile inputFile = preparePythonInputFile(baseDir, content); AnalysisConfiguration analysisConfig = AnalysisConfiguration.builder() .addInputFiles(inputFile) .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); List issues = new ArrayList<>(); var analyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, issues::add, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); analysisScheduler.post(analyzeCommand); analyzeCommand.getFutureResult().get(); assertThat(issues).hasSize(1); assertThat(issues) .extracting("ruleKey", "message", "inputFile", "flows", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset") .containsOnly(tuple(RuleKey.parse("python:S139"), "Move this trailing comment on the previous empty line.", inputFile, List.of(), 2, 9, 2, 27)); assertThat(issues.get(0).quickFixes()).hasSize(1); } @Test void should_fail_the_future_if_the_analyze_command_execution_fails() { var command = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> { throw new RuntimeException("Kaboom"); }, issue -> { }, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); analysisScheduler.post(command); assertThat(command.getFutureResult()).failsWithin(300, TimeUnit.MILLISECONDS) .withThrowableOfType(ExecutionException.class) .havingCause() .isInstanceOf(RuntimeException.class) .withMessage("Kaboom"); } @Test void should_cancel_progress_monitor_of_executing_analyze_command_when_stopping(@TempDir Path baseDir) throws IOException, InterruptedException { var content = """ def foo(): x = 9; # trailing comment """; ClientInputFile inputFile = preparePythonInputFile(baseDir, content); AnalysisConfiguration analysisConfig = AnalysisConfiguration.builder() .addInputFiles(inputFile) .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); var analyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, NO_OP_ISSUE_LISTENER, null, progressMonitor, TASK_MANAGER, inputFiles -> pause(300), ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); analysisScheduler.post(analyzeCommand); // let the engine run the first command Thread.sleep(100); analysisScheduler.stop(); engineStopped = true; await().until(analyzeCommand.getFutureResult()::isDone); assertThat(analyzeCommand.getFutureResult()) .isCancelled(); assertThat(progressMonitor.isCanceled()).isTrue(); } @Test void should_cancel_pending_commands_when_stopping(@TempDir Path baseDir) throws IOException, InterruptedException { var content = """ def foo(): x = 9; # trailing comment """; ClientInputFile inputFile = preparePythonInputFile(baseDir, content); AnalysisConfiguration analysisConfig = AnalysisConfiguration.builder() .addInputFiles(inputFile) .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); var analyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, NO_OP_ISSUE_LISTENER, null, progressMonitor, TASK_MANAGER, inputFiles -> pause(300), ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); var secondAnalyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, NO_OP_ISSUE_LISTENER, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); analysisScheduler.post(analyzeCommand); analysisScheduler.post(secondAnalyzeCommand); // let the engine run the first command Thread.sleep(100); analysisScheduler.stop(); engineStopped = true; await().until(analyzeCommand.getFutureResult()::isDone); assertThat(analyzeCommand.getFutureResult()) .isCancelled(); assertThat(secondAnalyzeCommand.getFutureResult()) .isCancelled(); assertThat(progressMonitor.isCanceled()).isTrue(); } @Test void should_not_fail_next_analysis_on_exception_from_command(@TempDir Path baseDir) throws IOException { Supplier throwingSupplier = () -> { throw new RuntimeException("Kaboom"); }; var content = """ def foo(): x = 9; # trailing comment """; var inputFile = preparePythonInputFile(baseDir, content); var analysisConfig = AnalysisConfiguration.builder() .addInputFiles(inputFile) .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); var issues1 = new ArrayList<>(); var issues2 = new ArrayList<>(); var analyzeCommand1 = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, issues1::add, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of("a", "1")); var throwingCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, NO_OP_ISSUE_LISTENER, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, throwingSupplier, Set.of(), Map.of("b", "2")); var analyzeCommand2 = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, issues2::add, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of("c", "3")); analysisScheduler.post(analyzeCommand1); analysisScheduler.post(throwingCommand); analysisScheduler.post(analyzeCommand2); await().untilAsserted(() -> assertThat(logTester.logs()).contains("Analysis command failed")); await().atMost(3, TimeUnit.SECONDS) .until(() -> analyzeCommand2.getFutureResult().isDone()); assertThat(issues2).hasSize(1); } @Test void should_not_queue_command_if_already_canceled(@TempDir Path baseDir) { var analysisConfig = AnalysisConfiguration.builder() .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); var analyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, i -> { }, null, progressMonitor, TASK_MANAGER, NO_OP_ANALYSIS_STARTED_CONSUMER, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of("a", "1")); progressMonitor.cancel(); analysisScheduler.post(analyzeCommand); await().untilAsserted(() -> assertThat(logTester.logs()).contains("Not picking next command " + analyzeCommand + ", is canceled")); } @Test void should_interrupt_executing_thread_when_stopping(@TempDir Path baseDir) throws IOException { var content = """ def foo(): x = 9; # trailing comment """; ClientInputFile inputFile = preparePythonInputFile(baseDir, content); AnalysisConfiguration analysisConfig = AnalysisConfiguration.builder() .addInputFiles(inputFile) .addActiveRules(trailingCommentRule()) .setBaseDir(baseDir) .build(); var threadTermination = new AtomicReference(); var analyzeCommand = new AnalyzeCommand("moduleKey", UUID.randomUUID(), TriggerType.FORCED, () -> analysisConfig, NO_OP_ISSUE_LISTENER, null, progressMonitor, TASK_MANAGER, inputFiles -> { try { Thread.sleep(3000); } catch (InterruptedException e) { threadTermination.set("INTERRUPTED"); return; } threadTermination.set("FINISHED"); }, ANALYSIS_READY_SUPPLIER, Set.of(), Map.of()); analysisScheduler.post(analyzeCommand); // let the engine run the first command pause(200); analysisScheduler.stop(); engineStopped = true; await().until(analyzeCommand.getFutureResult()::isDone); assertThat(threadTermination).hasValue("INTERRUPTED"); } @Test void should_not_log_any_error_when_stopping() { // let the engine block waiting for the first command pause(500); analysisScheduler.stop(); // let the engine stop properly pause(1000); assertThat(logTester.logs(LogOutput.Level.ERROR)).isEmpty(); } private ClientInputFile preparePythonInputFile(Path baseDir, String content) throws IOException { final var file = new File(baseDir.toFile(), "file.py"); FileUtils.write(file, content, StandardCharsets.UTF_8); return new OnDiskTestClientInputFile(file.toPath(), "file.py", false, StandardCharsets.UTF_8, SonarLanguage.PYTHON); } private static Path findPythonJarPath() throws IOException { var pluginsFolderPath = Paths.get("target/plugins/"); try (var files = Files.list(pluginsFolderPath)) { return files.filter(x -> x.getFileName().toString().endsWith(".jar")) .filter(x -> x.getFileName().toString().contains("python")) .findFirst().orElseThrow(() -> new RuntimeException("Unable to locate the python plugin")); } } private static ActiveRule trailingCommentRule() { return new ActiveRule() { @Override public RuleKey ruleKey() { return RuleKey.parse("python:S139"); } @Override public String severity() { return ""; } @Override public String language() { return "py"; } @CheckForNull @Override public String param(String key) { return params().get(key); } @Override public Map params() { return Map.of("legalTrailingCommentPattern", "^#\\s*+[^\\s]++$"); } @Override public String internalKey() { return ""; } @CheckForNull @Override public String templateRuleKey() { return null; } @Override public String qpKey() { return ""; } }; } private static ClientModuleFileSystem aModuleFileSystem() { return new ClientModuleFileSystem() { @Override public Stream files(String suffix, InputFile.Type type) { return Stream.of(); } @Override public Stream files() { return Stream.of(); } }; } private static void pause(long period) { try { Thread.sleep(period); } catch (InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultAnalysisErrorTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.TextPointer; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextPointer; import testutils.TestInputFileBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; class DefaultAnalysisErrorTests { private InputFile inputFile; private SensorStorage storage; private TextPointer textPointer; @BeforeEach void setUp() { inputFile = new TestInputFileBuilder("src/File.java").build(); textPointer = new DefaultTextPointer(5, 2); storage = mock(SensorStorage.class); } @Test void test_analysis_error() { var analysisError = new DefaultAnalysisError(storage); analysisError.onFile(inputFile) .at(textPointer) .message("msg"); assertThat(analysisError.location()).isEqualTo(textPointer); assertThat(analysisError.message()).isEqualTo("msg"); assertThat(analysisError.inputFile()).isEqualTo(inputFile); } @Test void test_save() { var analysisError = new DefaultAnalysisError(storage); analysisError.onFile(inputFile).save(); verify(storage).store(analysisError); verifyNoMoreInteractions(storage); } @Test void test_no_storage() { var analysisError = new DefaultAnalysisError(); assertThrows(NullPointerException.class, () -> analysisError.onFile(inputFile).save()); } @Test void test_validation() { assertThrows(IllegalArgumentException.class, () -> new DefaultAnalysisError(storage).onFile(null)); assertThrows(IllegalStateException.class, () -> new DefaultAnalysisError(storage).onFile(inputFile).onFile(inputFile)); assertThrows(IllegalStateException.class, () -> new DefaultAnalysisError(storage).at(textPointer).at(textPointer)); assertThrows(NullPointerException.class, () -> new DefaultAnalysisError(storage).save()); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultFilterableIssueTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.sonar.api.batch.fs.InputComponent; import org.sonar.api.batch.fs.TextRange; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextPointer; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextRange; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class DefaultFilterableIssueTests { @Test void delegate_textRange_to_rawIssue() { TextRange textRange = new DefaultTextRange(new DefaultTextPointer(0, 1), new DefaultTextPointer(2, 3)); var activeRule = mock(ActiveRule.class); when(activeRule.ruleKey()).thenReturn(RuleKey.of("foo", "S123")); var rawIssue = new Issue(activeRule, null, Map.of(), textRange, null, null, null, Optional.empty()); var underTest = new DefaultFilterableIssue(rawIssue, mock(InputComponent.class)); assertThat(underTest.textRange()).usingRecursiveComparison().isEqualTo(textRange); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSensorContextTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.sonar.api.SonarRuntime; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.rule.ActiveRules; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.config.Configuration; import org.sonar.api.config.Settings; import org.sonar.api.utils.Version; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewCoverage; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewCpdTokens; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewHighlighting; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewMeasure; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewSignificantCode; import org.sonarsource.sonarlint.core.analysis.sonarapi.noop.NoOpNewSymbolTable; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class DefaultSensorContextTests { @Mock private SonarLintInputProject module; @Mock private Settings settings; @Mock private Configuration config; @Mock private FileSystem fs; @Mock private ActiveRules activeRules; @Mock private SensorStorage sensorStorage; @Mock private SonarRuntime sqRuntime; private DefaultSensorContext ctx; private ProgressIndicator progressIndicator; private boolean canceled; @BeforeEach void setUp() { canceled = false; progressIndicator = new ProgressIndicator() { @Override public void notifyProgress(@Nullable String message, @Nullable Integer percentage) { // no-op } @Override public boolean isCanceled() { return canceled; } }; ctx = new DefaultSensorContext(module, settings, config, fs, activeRules, sensorStorage, sqRuntime, progressIndicator); } @Test void testGetters() { when(sqRuntime.getApiVersion()).thenReturn(Version.create(6, 1)); assertThat(ctx.activeRules()).isEqualTo(activeRules); assertThat(ctx.settings()).isEqualTo(settings); assertThat(ctx.config()).isEqualTo(config); assertThat(ctx.fileSystem()).isEqualTo(fs); assertThat(ctx.module()).isEqualTo(module); assertThat(ctx.runtime()).isEqualTo(sqRuntime); assertThat(ctx.getSonarQubeVersion()).isEqualTo(Version.create(6, 1)); assertThat(ctx.isCancelled()).isFalse(); // no ops assertThat(ctx.newCpdTokens()).isInstanceOf(NoOpNewCpdTokens.class); assertThat(ctx.newSymbolTable()).isInstanceOf(NoOpNewSymbolTable.class); assertThat(ctx.newHighlighting()).isInstanceOf(NoOpNewHighlighting.class); assertThat(ctx.newMeasure()).isInstanceOf(NoOpNewMeasure.class); assertThat(ctx.newCoverage()).isInstanceOf(NoOpNewCoverage.class); assertThat(ctx.newSignificantCode()).isInstanceOf(NoOpNewSignificantCode.class); ctx.addContextProperty(null, null); ctx.markForPublishing(null); assertThat(ctx.canSkipUnchangedFiles()).isFalse(); assertThat(ctx.isCacheEnabled()).isFalse(); assertThat(ctx.isFeatureAvailable("any")).isFalse(); assertThrows(UnsupportedOperationException.class, () -> ctx.newExternalIssue()); assertThrows(UnsupportedOperationException.class, () -> ctx.previousCache()); assertThrows(UnsupportedOperationException.class, () -> ctx.nextCache()); ctx.addTelemetryProperty("not", "applicable"); verify(sqRuntime).getApiVersion(); verifyNoMoreInteractions(sqRuntime); verifyNoInteractions(module); verifyNoInteractions(settings); verifyNoInteractions(fs); verifyNoInteractions(activeRules); verifyNoInteractions(sensorStorage); } @Test void testCancellation() { assertThat(ctx.isCancelled()).isFalse(); canceled = true; assertThat(ctx.isCancelled()).isTrue(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSensorDescriptorTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.util.Map; import org.junit.jupiter.api.Test; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import static org.assertj.core.api.Assertions.assertThat; class DefaultSensorDescriptorTests { @Test void describe() { var descriptor = new DefaultSensorDescriptor(); descriptor .name("Foo") .onlyOnLanguage("java") .onlyOnFileType(InputFile.Type.MAIN) .onlyWhenConfiguration(c -> c.hasKey("sonar.foo.reportPath") && c.hasKey("sonar.foo.reportPath2")) .createIssuesForRuleRepository("squid-java"); assertThat(descriptor.name()).isEqualTo("Foo"); assertThat(descriptor.languages()).containsOnly("java"); assertThat(descriptor.type()).isEqualTo(InputFile.Type.MAIN); var settings = new MapSettings(Map.of("sonar.foo.reportPath", "foo")); assertThat(descriptor.configurationPredicate().test(settings.asConfig())).isFalse(); settings = new MapSettings(Map.of("sonar.foo.reportPath", "foo", "sonar.foo.reportPath2", "foo")); assertThat(descriptor.configurationPredicate().test(settings.asConfig())).isTrue(); assertThat(descriptor.ruleRepositories()).containsOnly("squid-java"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultSonarLintIssueTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.rule.Severity; import org.sonar.api.batch.sensor.internal.SensorStorage; import org.sonar.api.batch.sensor.issue.MessageFormatting; import org.sonar.api.batch.sensor.issue.fix.NewInputFileEdit; import org.sonar.api.batch.sensor.issue.fix.NewQuickFix; import org.sonar.api.batch.sensor.issue.fix.NewTextEdit; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputDir; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputProject; import testutils.TestInputFileBuilder; import static org.apache.commons.lang3.StringUtils.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; class DefaultSonarLintIssueTests { private SonarLintInputProject project; private final InputFile inputFile = new TestInputFileBuilder("src/Foo.php") .initMetadata("Foo\nBar\n") .build(); @TempDir private Path baseDir; @BeforeEach void prepare() { project = new SonarLintInputProject(); } @Test void build_file_issue() { var storage = mock(SensorStorage.class); var range = inputFile.selectLine(1); var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(inputFile) .at(range) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .gap(10.0) .overrideImpact(SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.HIGH); assertThat(issue.primaryLocation().inputComponent()).isEqualTo(inputFile); assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); assertThat(issue.primaryLocation().textRange().start().line()).isEqualTo(1); assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!"); assertThat(issue.overridenImpacts()).containsExactly(entry(SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.HIGH)); assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(issue::gap) .withMessage("No gap in SonarLint"); var newQuickFix = issue.newQuickFix().message("Fix this issue"); var newInputFileEdit = newQuickFix.newInputFileEdit().on(inputFile); newInputFileEdit.addTextEdit((NewTextEdit) newInputFileEdit.newTextEdit().at(range).withNewText("// Fixed!")); newQuickFix.addInputFileEdit((NewInputFileEdit) newInputFileEdit); issue.addQuickFix((NewQuickFix) newQuickFix); var quickFixes = issue.quickFixes(); assertThat(quickFixes).hasSize(1); var quickFix = quickFixes.get(0); assertThat(quickFix.message()).isEqualTo("Fix this issue"); var inputFileEdits = quickFix.inputFileEdits(); assertThat(inputFileEdits).hasSize(1); var inputFileEdit = inputFileEdits.get(0); assertThat(inputFileEdit.target()).isEqualTo(inputFile); assertThat(inputFileEdit.textEdits()).hasSize(1); var textEdit = inputFileEdit.textEdits().get(0); assertThat(textEdit.range().start().line()).isEqualTo(range.start().line()); assertThat(textEdit.range().start().lineOffset()).isEqualTo(range.start().lineOffset()); assertThat(textEdit.range().end().line()).isEqualTo(range.end().line()); assertThat(textEdit.range().end().lineOffset()).isEqualTo(range.end().lineOffset()); assertThat(textEdit.newText()).isEqualTo("// Fixed!"); issue.save(); verify(storage).store(issue); } @Test void replace_null_characters() { var storage = mock(SensorStorage.class); var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(inputFile) .message("Wrong \u0000 use of NULL\u0000")) .forRule(RuleKey.of("repo", "rule")); assertThat(issue.primaryLocation().message()).isEqualTo("Wrong [NULL] use of NULL[NULL]"); issue.save(); verify(storage).store(issue); } @Test void truncate_and_trim() { var storage = mock(SensorStorage.class); var prefix = "prefix: "; var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(inputFile) .message(" " + prefix + repeat("a", 4000))) .forRule(RuleKey.of("repo", "rule")); var ellipse = "..."; assertThat(issue.primaryLocation().message()).isEqualTo(prefix + repeat("a", 4000 - prefix.length() - ellipse.length()) + ellipse); issue.save(); verify(storage).store(issue); } @Test void ignore_formatting_and_keep_unformatted_message() { var storage = mock(SensorStorage.class); var location = new DefaultSonarLintIssueLocation(); var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(location .on(inputFile) .message("formattedMessage", List.of(location.newMessageFormatting() .start(1) .end(2) .type(MessageFormatting.Type.CODE)))) .forRule(RuleKey.of("repo", "rule")); assertThat(issue.primaryLocation().message()).isEqualTo("formattedMessage"); assertThat(issue.primaryLocation().messageFormattings()).isEmpty(); } @Test void move_directory_issue_to_project_root() { var storage = mock(SensorStorage.class); var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(new SonarLintInputDir(baseDir.resolve("src/main"))) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .overrideSeverity(Severity.BLOCKER); assertThat(issue.primaryLocation().inputComponent()).isEqualTo(project); assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); assertThat(issue.primaryLocation().textRange()).isNull(); assertThat(issue.primaryLocation().message()).isEqualTo("[src/main] Wrong way!"); assertThat(issue.overriddenSeverity()).isEqualTo(Severity.BLOCKER); issue.save(); verify(storage).store(issue); } @Test void build_project_issue() { var storage = mock(SensorStorage.class); var issue = new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(project) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .gap(10.0); assertThat(issue.primaryLocation().inputComponent()).isEqualTo(project); assertThat(issue.ruleKey()).isEqualTo(RuleKey.of("repo", "rule")); assertThat(issue.primaryLocation().textRange()).isNull(); assertThat(issue.primaryLocation().message()).isEqualTo("Wrong way!"); issue.save(); verify(storage).store(issue); } @Test void does_not_support_variants() { var storage = mock(SensorStorage.class); var issue = (DefaultSonarLintIssue) new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(project) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .setCodeVariants(List.of("variant1", "variant2")) .gap(10.0); assertThat(issue.codeVariants()).isEmpty(); } @Test void supports_adding_internal_tags_one_by_one() { var storage = mock(SensorStorage.class); var issue = (DefaultSonarLintIssue) new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(project) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .addInternalTag("tag1") .addInternalTag("tag2"); assertThat(issue.internalTags()).containsExactly("tag1", "tag2"); } @Test void supports_adding_many_internal_tags() { var storage = mock(SensorStorage.class); var issue = (DefaultSonarLintIssue) new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(project) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .addInternalTags(List.of("tag1", "tag2")) .addInternalTags(List.of("tag3")); assertThat(issue.internalTags()).containsExactly("tag1", "tag2", "tag3"); } @Test void supports_setting_many_internal_tags() { var storage = mock(SensorStorage.class); var issue = (DefaultSonarLintIssue) new DefaultSonarLintIssue(project, baseDir, storage) .at(new DefaultSonarLintIssueLocation() .on(project) .message("Wrong way!")) .forRule(RuleKey.of("repo", "rule")) .setInternalTags(List.of("tag1", "tag2")); assertThat(issue.internalTags()).containsExactly("tag1", "tag2"); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/DefaultTempFolderTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi; import java.io.File; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class DefaultTempFolderTests { @RegisterExtension SonarLintLogTester logTester = new SonarLintLogTester(); @Test void createTempFolderAndFile(@TempDir File rootTempFolder) { var underTest = new DefaultTempFolder(rootTempFolder); var dir = underTest.newDir(); assertThat(dir).exists().isDirectory(); var file = underTest.newFile(); assertThat(file).exists().isFile(); underTest.clean(); assertThat(rootTempFolder).doesNotExist(); } @Test void createTempFolderWithName(@TempDir File rootTempFolder) { var underTest = new DefaultTempFolder(rootTempFolder); var dir = underTest.newDir("sample"); assertThat(dir).exists().isDirectory(); assertThat(new File(rootTempFolder, "sample")).isEqualTo(dir); underTest.clean(); assertThat(rootTempFolder).doesNotExist(); } @Test void newDir_throws_ISE_if_name_is_not_valid(@TempDir File rootTempFolder) { var underTest = new DefaultTempFolder(rootTempFolder); var tooLong = new StringBuilder("tooooolong"); for (var i = 0; i < 50; i++) { tooLong.append("tooooolong"); } var tooLongString = tooLong.toString(); var thrown = assertThrows(IllegalStateException.class, () -> underTest.newDir(tooLongString)); assertThat(thrown).hasMessageStartingWith("Failed to create temp directory"); } @Test void newFile_throws_ISE_if_name_is_not_valid(@TempDir File rootTempFolder) { var underTest = new DefaultTempFolder(rootTempFolder); var tooLong = new StringBuilder("tooooolong"); for (var i = 0; i < 50; i++) { tooLong.append("tooooolong"); } var tooLongString = tooLong.toString(); var thrown = assertThrows(IllegalStateException.class, () -> underTest.newFile(tooLongString, ".txt")); assertThat(thrown).hasMessage("Failed to create temp file"); } @Test void clean_deletes_non_empty_directory(@TempDir File dir) throws Exception { FileUtils.touch(new File(dir, "foo.txt")); var underTest = new DefaultTempFolder(dir); underTest.clean(); assertThat(dir).doesNotExist(); } @Test void clean_does_not_fail_if_directory_has_already_been_deleted(@TempDir File dir) { var underTest = new DefaultTempFolder(dir); underTest.clean(); assertThat(dir).doesNotExist(); // second call does not fail, nor log ERROR logs underTest.clean(); assertThat(logTester.logs(Level.ERROR)).isEmpty(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewCoverageTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewCoverageTests { @Test void test() { new NoOpNewCoverage() .onFile(null) .conditions(0, 0, 0) .lineHits(0, 0) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewCpdTokensTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewCpdTokensTests { @Test void improve_coverage() { new NoOpNewCpdTokens() .onFile(null) .addToken(null, null) .addToken(0, 0, 0, 0, null) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewHighlightingTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewHighlightingTests { @Test void improve_coverage() { new NoOpNewHighlighting().onFile(null) .highlight(null, null) .highlight(0, 0, 0, 0, null) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewMeasureTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewMeasureTests { @Test void test() { new NoOpNewMeasure<>() .on(null) .forMetric(null) .withValue(null) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewMessageFormattingTest.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; import org.sonar.api.batch.sensor.issue.MessageFormatting; import static org.assertj.core.api.Assertions.assertThat; class NoOpNewMessageFormattingTest { @Test void should_do_nothing_and_return_same_instance() { var originalMessageFormatting = new NoOpNewMessageFormatting(); var messageFormatting = originalMessageFormatting .start(1) .end(4) .type(MessageFormatting.Type.CODE); assertThat(messageFormatting).isEqualTo(originalMessageFormatting); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewSignificantCodeTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewSignificantCodeTests { @Test void visit_all_builder_fields() { new NoOpNewSignificantCode() .onFile(null) .addRange(null) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/sonarapi/noop/NoOpNewSymbolTableTests.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis.sonarapi.noop; import org.junit.jupiter.api.Test; class NoOpNewSymbolTableTests { @Test void improve_coverage() { new NoOpNewSymbolTable() .onFile(null) .newReference(null) .newReference(0, 0, 0, 0) .newSymbol(null) .newSymbol(0, 0, 0, 0) .save(); } } ================================================ FILE: backend/analysis-engine/src/test/java/testutils/FileUtils.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.io.File; import java.util.regex.Pattern; public class FileUtils { private static final String PATH_SEPARATOR_PATTERN = Pattern.quote(File.separator); /** * Converts path to format used by SonarQube * * @param path path string in the local OS * @return SonarQube path */ public static String toSonarQubePath(String path) { if (File.separatorChar != '/') { return path.replaceAll(PATH_SEPARATOR_PATTERN, "/"); } return path; } } ================================================ FILE: backend/analysis-engine/src/test/java/testutils/InMemoryTestClientInputFile.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonar.api.utils.PathUtils; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class InMemoryTestClientInputFile implements ClientInputFile { private final boolean isTest; private final SonarLanguage language; private final String relativePath; private final String contents; private final Path path; public InMemoryTestClientInputFile(String contents, String relativePath, @Nullable Path path, final boolean isTest, @Nullable SonarLanguage language) { this.contents = contents; this.relativePath = relativePath; this.path = path; this.isTest = isTest; this.language = language; } @Override public String getPath() { if (path == null) { throw new UnsupportedOperationException("getPath"); } return PathUtils.sanitize(path.toString()); } @Override public String relativePath() { return relativePath; } @Override public boolean isTest() { return isTest; } @Override public SonarLanguage language() { return language; } @Override public Charset getCharset() { return StandardCharsets.UTF_8; } @Override public G getClientObject() { return null; } @Override public InputStream inputStream() throws IOException { return new ByteArrayInputStream(relativePath.getBytes(StandardCharsets.UTF_8)); } @Override public String contents() throws IOException { return contents; } @Override public URI uri() { if (path == null) { return URI.create("file://" + relativePath); } return path.toUri(); } } ================================================ FILE: backend/analysis-engine/src/test/java/testutils/OnDiskTestClientInputFile.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class OnDiskTestClientInputFile implements ClientInputFile { private final Path path; private final boolean isTest; private final Charset encoding; private final SonarLanguage language; private final String relativePath; public OnDiskTestClientInputFile(final Path path, String relativePath, final boolean isTest, final Charset encoding) { this(path, relativePath, isTest, encoding, null); } public OnDiskTestClientInputFile(final Path path, String relativePath, final boolean isTest, final Charset encoding, @Nullable SonarLanguage language) { this.path = path; this.relativePath = relativePath; this.isTest = isTest; this.encoding = encoding; this.language = language; } @Override public String getPath() { return path.toString(); } @Override public String relativePath() { return relativePath; } @Override public boolean isTest() { return isTest; } @Override public SonarLanguage language() { return language; } @Override public Charset getCharset() { return encoding; } @Override public G getClientObject() { return null; } @Override public InputStream inputStream() throws IOException { return Files.newInputStream(path); } @Override public String contents() throws IOException { return new String(Files.readAllBytes(path), encoding); } @Override public URI uri() { return path.toUri(); } } ================================================ FILE: backend/analysis-engine/src/test/java/testutils/TestClientInputFile.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class TestClientInputFile implements ClientInputFile { private final Path path; private final boolean isTest; private final Charset encoding; private final SonarLanguage language; private final Path baseDir; public TestClientInputFile(final Path baseDir, final Path path, final boolean isTest, final Charset encoding, @Nullable SonarLanguage language) { this.baseDir = baseDir; this.path = path; this.isTest = isTest; this.encoding = encoding; this.language = language; } @Override public String getPath() { return path.toString(); } @Override public String relativePath() { return baseDir.relativize(path).toString(); } @Override public boolean isTest() { return isTest; } @Override public Charset getCharset() { return encoding; } @Override public G getClientObject() { return null; } @Override public InputStream inputStream() throws IOException { return Files.newInputStream(path); } @Override public String contents() throws IOException { return new String(Files.readAllBytes(path), encoding); } @Override public SonarLanguage language() { return language; } @Override public URI uri() { return path.toUri(); } } ================================================ FILE: backend/analysis-engine/src/test/java/testutils/TestInputFileBuilder.java ================================================ /* * SonarLint Core - Analysis Engine * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.io.ByteArrayInputStream; import java.io.File; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.utils.PathUtils; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.FileMetadata; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; /** * Intended to be used in unit tests that need to create {@link InputFile}s. * An InputFile is unambiguously identified by a module key and a relative path, so these parameters are mandatory. *

* A module base directory is only needed to construct absolute paths. *

* Examples of usage of the constructors: * *

 * InputFile file1 = TestInputFileBuilder.create("module1", "myfile.java").build();
 * InputFile file2 = TestInputFileBuilder.create("", fs.baseDir(), myfile).build();
 * 
*

* file1 will have the "module1" as both module key and module base directory. * file2 has an empty string as module key, and a relative path which is the path from the filesystem base directory to myfile. */ public class TestInputFileBuilder { private final String relativePath; @CheckForNull private Path baseDir; private SonarLanguage language; private InputFile.Type type = InputFile.Type.MAIN; private int lines = -1; private int[] originalLineStartOffsets = new int[0]; private int lastValidOffset = -1; private String contents; /** * Create a InputFile with a given module key and module base directory. * The relative path is generated comparing the file path to the module base directory. * filePath must point to a file that is within the module base directory. */ public TestInputFileBuilder(File baseDir, File filePath) { var relativePathStr = baseDir.toPath().relativize(filePath.toPath()).toString(); setBaseDir(baseDir.toPath()); this.relativePath = PathUtils.sanitize(relativePathStr); } public TestInputFileBuilder(String relativePath) { this.relativePath = PathUtils.sanitize(relativePath); } public static TestInputFileBuilder create(File moduleBaseDir, File filePath) { return new TestInputFileBuilder(moduleBaseDir, filePath); } public static TestInputFileBuilder create(String relativePath) { return new TestInputFileBuilder(relativePath); } public TestInputFileBuilder setBaseDir(Path baseDir) { this.baseDir = baseDir; return this; } public TestInputFileBuilder setLanguage(@Nullable SonarLanguage language) { this.language = language; return this; } public TestInputFileBuilder setType(InputFile.Type type) { this.type = type; return this; } public TestInputFileBuilder setLines(int lines) { this.lines = lines; return this; } /** * Set contents of the file and calculates metadata from it. * The contents will be returned by {@link InputFile#contents()} and {@link InputFile#inputStream()} and can be * inconsistent with the actual physical file pointed by {@link InputFile#path()}, {@link InputFile#absolutePath()}, etc. */ public TestInputFileBuilder setContents(String content) { this.contents = content; initMetadata(content); return this; } public TestInputFileBuilder setLastValidOffset(int lastValidOffset) { this.lastValidOffset = lastValidOffset; return this; } public TestInputFileBuilder setOriginalLineStartOffsets(int[] originalLineStartOffsets) { this.originalLineStartOffsets = originalLineStartOffsets; return this; } public TestInputFileBuilder setMetadata(FileMetadata.Metadata metadata) { this.setLines(metadata.lines()); this.setLastValidOffset(metadata.lastValidOffset()); this.setOriginalLineStartOffsets(metadata.originalLineOffsets()); return this; } public TestInputFileBuilder initMetadata(String content) { return setMetadata( new FileMetadata().readMetadata(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8, URI.create("file://test"), null)); } public SonarLintInputFile build() { ClientInputFile clientInputFile = new InMemoryTestClientInputFile(contents, relativePath, baseDir != null ? baseDir.resolve(relativePath) : null, type == Type.TEST, language); return new SonarLintInputFile(clientInputFile, f -> new FileMetadata.Metadata(lines, originalLineStartOffsets, lastValidOffset)) .setType(type) .setLanguage(language); } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-double-regexp-mess.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } // SONAR-OFF public boolean in(int lineId) { return from <= lineId && lineId <= to; } // FOO-OFF public Set toLines() { Set lines = Sets.newLinkedHashSet(); // SONAR-ON for (int index = from; index <= to; index++) { lines.add(index); } // FOO-ON return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-double-regexp-twice.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } // SONAR-OFF public boolean in(int lineId) { return from <= lineId && lineId <= to; } // SONAR-ON public Set toLines() { Set lines = Sets.newLinkedHashSet(); // FOO-OFF for (int index = from; index <= to; index++) { lines.add(index); } // FOO-ON return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-double-regexp-unfinished.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } // SONAR-OFF public boolean in(int lineId) { return from <= lineId && lineId <= to; } public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-double-regexp-wrong-order.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } // SONAR-ON public boolean in(int lineId) { return from <= lineId && lineId <= to; } // SONAR-OFF public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-double-regexp.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } // SONAR-OFF public boolean in(int lineId) { return from <= lineId && lineId <= to; } // SONAR-ON public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-no-regexp.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } public boolean in(int lineId) { return from <= lineId && lineId <= to; } public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-single-regexp-and-double-regexp.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; // SONAR-OFF import java.util.Set; /** * @SONAR-IGNORE-ALL */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } public boolean in(int lineId) { return from <= lineId && lineId <= to; } // SONAR-ON public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-single-regexp-last-line.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } public boolean in(int lineId) { return from <= lineId && lineId <= to; } public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } // @SONAR-IGNORE-ALL ================================================ FILE: backend/analysis-engine/src/test/resources/IssueExclusionsRegexpScannerTests/file-with-single-regexp.txt ================================================ package org.sonar.plugins.switchoffviolations.pattern; import com.google.common.collect.Sets; import java.util.Set; /** * @SONAR-IGNORE-ALL */ public class LineRange { int from, to; public LineRange(int from, int to) { if (to < from) { throw new IllegalArgumentException("Line range is not valid: " + from + " must be greater than " + to); } this.from = from; this.to = to; } public boolean in(int lineId) { return from <= lineId && lineId <= to; } public Set toLines() { Set lines = Sets.newLinkedHashSet(); for (int index = from; index <= to; index++) { lines.add(index); } return lines; } } ================================================ FILE: backend/analysis-engine/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/cli/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-backend-cli SonarLint Core - Backend CLI SonarLint backend as a standalone CLI ${project.build.directory}/unpack jdk-21.0.10+7-jre jdk-21.0.10+7-jre jdk-21.0.10+7-jre/Contents/Home org.apache.maven.plugins maven-jar-plugin org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli true dist-no-arch maven-assembly-plugin assemble-no-arch package single \ src/main/assembly/dist-no-arch.xml windows dist-windows_x64 com.googlecode.maven-download-plugin download-maven-plugin unpack-windows_x64 package wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.10%2B7/OpenJDK21U-jre_x64_windows_hotspot_21.0.10_7.zip true ${unpack.dir}/windows_x64 a6ac6789e51a2c245f41430c42e72b39ec706a449812fc5e4cbfc55ceed1e5ae maven-assembly-plugin assemble-windows_x64 package single \ src/main/assembly/dist-windows_x64.xml unix dist-linux_x64 com.googlecode.maven-download-plugin download-maven-plugin unpack-linux_x64 package wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.10%2B7/OpenJDK21U-jre_x64_linux_hotspot_21.0.10_7.tar.gz true ${unpack.dir}/linux_x64 991be6ac6725e76109ecbd131d658f992dcbeacba3a8b4b6650302c8012b52fb maven-assembly-plugin assemble-linux_x64 package single \ src/main/assembly/dist-linux_x64.xml dist-linux_aarch64 com.googlecode.maven-download-plugin download-maven-plugin unpack-linux_aarch64 package wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.10%2B7/OpenJDK21U-jre_aarch64_linux_hotspot_21.0.10_7.tar.gz true ${unpack.dir}/linux_aarch64 3ca84da7c4f57eee8d7e7f0645dc904a3a06456d32b37a4dd57a5e7527245250 maven-assembly-plugin assemble-linux_aarch64 package single \ src/main/assembly/dist-linux_aarch64.xml dist-macosx_x64 com.googlecode.maven-download-plugin download-maven-plugin unpack-macosx_x64 package wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.10%2B7/OpenJDK21U-jre_x64_mac_hotspot_21.0.10_7.tar.gz true ${unpack.dir}/macosx_x64 008d2bb904c0e07500b92bf4b0f8d434d694b13d5189f06358a52d46b1351f37 maven-assembly-plugin assemble-macosx_x64 package single \ src/main/assembly/dist-macosx_x64.xml dist-macosx_aarch64 com.googlecode.maven-download-plugin download-maven-plugin unpack-macosx_aarch64 package wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.10%2B7/OpenJDK21U-jre_aarch64_mac_hotspot_21.0.10_7.tar.gz true ${unpack.dir}/macosx_aarch64 c3be8c87f1a5cdc727903546eb810e112f94cd7222dac6a9d3f3146ee932008d maven-assembly-plugin assemble-macosx_aarch64 package single \ src/main/assembly/dist-macosx_aarch64.xml com.google.code.findbugs jsr305 provided info.picocli picocli 4.7.7 ${project.groupId} sonarlint-rpc-impl ${project.version} org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test ================================================ FILE: backend/cli/src/main/assembly/dist-linux_aarch64.xml ================================================ linux_aarch64 false tar.gz /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ${unpack.dir}/linux_aarch64/${jre.dirname.linux} jre bin/** man/** lib/jspawnhelper lib/jexec plugin/** ${unpack.dir}/linux_aarch64/${jre.dirname.linux}/bin jre/bin java 0755 ${unpack.dir}/linux_aarch64/${jre.dirname.linux}/lib jre/lib jspawnhelper jexec 0755 ================================================ FILE: backend/cli/src/main/assembly/dist-linux_x64.xml ================================================ linux_x64 false tar.gz /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ${unpack.dir}/linux_x64/${jre.dirname.linux} jre bin/** man/** lib/jspawnhelper lib/jexec plugin/** ${unpack.dir}/linux_x64/${jre.dirname.linux}/bin jre/bin java 0755 ${unpack.dir}/linux_x64/${jre.dirname.linux}/lib jre/lib jspawnhelper jexec 0755 ================================================ FILE: backend/cli/src/main/assembly/dist-macosx_aarch64.xml ================================================ macosx_aarch64 false tar.gz /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ${unpack.dir}/macosx_aarch64/${jre.dirname.macosx} jre bin/** man/** lib/jspawnhelper ${unpack.dir}/macosx_aarch64/${jre.dirname.macosx}/bin jre/bin java 0755 ${unpack.dir}/macosx_aarch64/${jre.dirname.macosx}/lib jre/lib jspawnhelper 0755 ================================================ FILE: backend/cli/src/main/assembly/dist-macosx_x64.xml ================================================ macosx_x64 false tar.gz /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ${unpack.dir}/macosx_x64/${jre.dirname.macosx} jre bin/** man/** lib/jspawnhelper ${unpack.dir}/macosx_x64/${jre.dirname.macosx}/bin jre/bin java 0755 ${unpack.dir}/macosx_x64/${jre.dirname.macosx}/lib jre/lib jspawnhelper 0755 ================================================ FILE: backend/cli/src/main/assembly/dist-no-arch.xml ================================================ no-arch false zip /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ================================================ FILE: backend/cli/src/main/assembly/dist-windows_x64.xml ================================================ windows_x64 false zip /lib false ${artifact} ${project.build.directory} lib 0644 **/${project.artifactId}-${project.version}.jar ${unpack.dir}/windows_x64/${jre.dirname.windows} jre bin/** man/** plugin/** ${unpack.dir}/windows_x64/${jre.dirname.windows}/bin jre/bin 0755 ================================================ FILE: backend/cli/src/main/java/org/sonarsource/sonarlint/core/backend/cli/EndOfStreamAwareInputStream.java ================================================ /* * SonarLint Core - Backend CLI * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.backend.cli; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.CompletableFuture; public class EndOfStreamAwareInputStream extends InputStream { private final InputStream delegate; private final CompletableFuture onExit = new CompletableFuture<>(); public EndOfStreamAwareInputStream(InputStream delegate) { this.delegate = delegate; } public CompletableFuture onExit() { return onExit; } @Override public int read() throws IOException { return exitIfNegative(delegate::read); } @Override public int read(byte[] b) throws IOException { return exitIfNegative(() -> delegate.read(b)); } @Override public int read(byte[] b, int off, int len) throws IOException { return exitIfNegative(() -> delegate.read(b, off, len)); } private int exitIfNegative(SupplierWithIOException call) throws IOException { int result = call.get(); if (result < 0) { onExit.complete(null); } return result; } @FunctionalInterface private interface SupplierWithIOException { /** * @return result */ T get() throws IOException; } } ================================================ FILE: backend/cli/src/main/java/org/sonarsource/sonarlint/core/backend/cli/SonarLintServerCli.java ================================================ /* * SonarLint Core - Backend CLI * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.backend.cli; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.PrintStream; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import picocli.CommandLine; @CommandLine.Command(name = "slcore", mixinStandardHelpOptions = true, description = "The SonarLint Core backend") public class SonarLintServerCli implements Callable { @Override public Integer call() { return run(System.in, System.out); } int run(InputStream originalStdIn, PrintStream originalStdOut) { var inputStream = new EndOfStreamAwareInputStream(originalStdIn); System.setIn(new ByteArrayInputStream(new byte[0])); // Redirect all logs to stderr for now, would be better to go to a file later System.setOut(System.err); try { var rpcLauncher = new BackendJsonRpcLauncher(inputStream, originalStdOut); var rpcServer = rpcLauncher.getServer(); inputStream.onExit().thenRun(() -> { if (!rpcServer.isReaderShutdown()) { System.err.println("Input stream has closed, exiting..."); rpcServer.shutdown(); } }); rpcServer.getClientListener().get(); } catch (CancellationException shutdown) { System.err.println("Server is shutting down..."); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); return -1; } catch (Exception e) { e.printStackTrace(); return -1; } return 0; } public static void main(String... args) { var exitCode = new CommandLine(new SonarLintServerCli()).execute(args); System.exit(exitCode); } } ================================================ FILE: backend/cli/src/main/java/org/sonarsource/sonarlint/core/backend/cli/package-info.java ================================================ /* * SonarLint Core - Backend CLI * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.backend.cli; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/cli/src/test/java/org/sonarsource/sonarlint/core/backend/cli/EndOfStreamAwareInputStreamTest.java ================================================ /* * SonarLint Core - Backend CLI * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.backend.cli; import java.io.ByteArrayInputStream; import java.io.IOException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class EndOfStreamAwareInputStreamTest { @Test void it_should_complete_onExit_when_reading_single_byte_and_stream_is_empty() throws IOException { var stream = new EndOfStreamAwareInputStream(new ByteArrayInputStream(new byte[0])); var bytesRead = stream.read(); assertThat(bytesRead).isEqualTo(-1); assertThat(stream.onExit()).isCompleted(); } @Test void it_should_complete_onExit_when_reading_byte_array_and_stream_is_empty() throws IOException { var stream = new EndOfStreamAwareInputStream(new ByteArrayInputStream(new byte[0])); var bytesRead = stream.read(new byte[5]); assertThat(bytesRead).isEqualTo(-1); assertThat(stream.onExit()).isCompleted(); } @Test void it_should_complete_onExit_when_reading_byte_array_slice_and_stream_is_empty() throws IOException { var stream = new EndOfStreamAwareInputStream(new ByteArrayInputStream(new byte[0])); var bytesRead = stream.read(new byte[5], 0, 3); assertThat(bytesRead).isEqualTo(-1); assertThat(stream.onExit()).isCompleted(); } @Test void it_should_not_complete_onExit_if_stream_is_not_empty() throws IOException { var stream = new EndOfStreamAwareInputStream(new ByteArrayInputStream(new byte[] {0b01})); var bytesRead = stream.read(); assertThat(bytesRead).isEqualTo(1); assertThat(stream.onExit()).isNotCompleted(); } } ================================================ FILE: backend/cli/src/test/java/org/sonarsource/sonarlint/core/backend/cli/SonarLintServerCliTest.java ================================================ /* * SonarLint Core - Backend CLI * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.backend.cli; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.impl.SonarLintRpcServerImpl; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstructionWithAnswer; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; class SonarLintServerCliTest { @Test void it_should_return_success_exit_code_when_parent_stream_ends() { var exitCode = new SonarLintServerCli().run(new ByteArrayInputStream(new byte[0]), new PrintStream(new ByteArrayOutputStream())); assertThat(exitCode).isZero(); } @Test void log_when_client_is_closed() throws IOException { var outContent = new ByteArrayOutputStream(); System.setErr(new PrintStream(outContent)); var inputStream = spy(new ByteArrayInputStream(new byte[0])); when(inputStream.available()).thenReturn(1); var exitCode = new SonarLintServerCli().run(inputStream, new PrintStream(new ByteArrayOutputStream())); assertThat(outContent.toString()).isEqualToIgnoringNewLines("Input stream has closed, exiting..."); assertThat(exitCode).isZero(); outContent.close(); } @Test void log_when_connection_canceled() { var outContent = new ByteArrayOutputStream(); System.setErr(new PrintStream(outContent)); var mockServer = mock(SonarLintRpcServerImpl.class); doThrow(CancellationException.class).when(mockServer).getClientListener(); try (var ignored = mockConstructionWithAnswer(BackendJsonRpcLauncher.class, invocationOnMock -> mockServer)) { var exitCode = new SonarLintServerCli().run(new ByteArrayInputStream(new byte[0]), new PrintStream(new ByteArrayOutputStream())); assertThat(outContent.toString()).isEqualToIgnoringNewLines("Server is shutting down..."); assertThat(exitCode).isZero(); } } @Test void log_interrupted_exception() throws ExecutionException, InterruptedException { var outContent = new ByteArrayOutputStream(); System.setErr(new PrintStream(outContent)); var mockServer = mock(SonarLintRpcServerImpl.class); var mockFuture = mock(Future.class); when(mockServer.getClientListener()).thenReturn(mockFuture); doThrow(new InterruptedException("interrupted exc")).when(mockFuture).get(); try (var ignored = mockConstructionWithAnswer(BackendJsonRpcLauncher.class, invocationOnMock -> mockServer)) { var exitCode = new SonarLintServerCli().run(new ByteArrayInputStream(new byte[0]), new PrintStream(new ByteArrayOutputStream())); assertThat(outContent.toString()).contains("java.lang.InterruptedException: interrupted exc"); assertThat(exitCode).isEqualTo(-1); } } @Test void log_other_exceptions() { var outContent = new ByteArrayOutputStream(); System.setErr(new PrintStream(outContent)); var mockServer = mock(SonarLintRpcServerImpl.class); doThrow(new RuntimeException("an exc")).when(mockServer).getClientListener(); try (var ignored = mockConstructionWithAnswer(BackendJsonRpcLauncher.class, invocationOnMock -> mockServer)) { var exitCode = new SonarLintServerCli().run(new ByteArrayInputStream(new byte[0]), new PrintStream(new ByteArrayOutputStream())); assertThat(outContent.toString()).contains("java.lang.RuntimeException: an exc"); assertThat(exitCode).isEqualTo(-1); } } } ================================================ FILE: backend/commons/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-commons SonarLint Core - Commons Common code for all SonarLint modules com.google.code.findbugs jsr305 provided com.h2database h2 org.jooq jooq org.flywaydb flyway-core org.sonarsource.git.blame git-files-blame 2.0.2.54 org.eclipse.jgit org.eclipse.jgit ${jgit7.version} commons-io commons-io com.google.code.gson gson org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test org.assertj assertj-core test org.mockito mockito-core test org.awaitility awaitility test com.squareup.okhttp3 mockwebserver3 test ch.qos.logback logback-classic test io.sentry sentry ${sentry.version} jakarta.inject jakarta.inject-api org.apache.commons commons-lang3 src/main/resources sl_core_version.txt true src/main/resources sl_core_version.txt false org.flywaydb flyway-maven-plugin generate-sources migrate jdbc:h2:${project.build.directory}/db/for-schema-generation.db user filesystem:src/main/resources/db/migration/ org.jooq jooq-codegen-maven generate-sources generate org.h2.Driver jdbc:h2:${project.build.directory}/db/for-schema-generation.db user org.jooq.meta.h2.H2Database PUBLIC org.sonarsource.sonarlint.core.commons.storage.model ${project.build.directory}/generated-sources/jooq org.apache.maven.plugins maven-jar-plugin test-jar ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/CleanCodeAttribute.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import static org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory.ADAPTABLE; import static org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory.CONSISTENT; import static org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory.INTENTIONAL; import static org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory.RESPONSIBLE; public enum CleanCodeAttribute { CONVENTIONAL(CONSISTENT), FORMATTED(CONSISTENT), IDENTIFIABLE(CONSISTENT), CLEAR(INTENTIONAL), COMPLETE(INTENTIONAL), EFFICIENT(INTENTIONAL), LOGICAL(INTENTIONAL), DISTINCT(ADAPTABLE), FOCUSED(ADAPTABLE), MODULAR(ADAPTABLE), TESTED(ADAPTABLE), LAWFUL(RESPONSIBLE), RESPECTFUL(RESPONSIBLE), TRUSTWORTHY(RESPONSIBLE); private final CleanCodeAttributeCategory attributeCategory; CleanCodeAttribute(CleanCodeAttributeCategory attributeCategory) { this.attributeCategory = attributeCategory; } public CleanCodeAttributeCategory getAttributeCategory() { return attributeCategory; } public static CleanCodeAttribute defaultCleanCodeAttribute() { return CONVENTIONAL; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/CleanCodeAttributeCategory.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum CleanCodeAttributeCategory { ADAPTABLE, CONSISTENT, INTENTIONAL, RESPONSIBLE } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/ConnectionKind.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum ConnectionKind { SONARQUBE, SONARCLOUD } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/HotspotReviewStatus.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.Arrays; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import static org.sonarsource.sonarlint.core.commons.ConnectionKind.SONARCLOUD; import static org.sonarsource.sonarlint.core.commons.ConnectionKind.SONARQUBE; public enum HotspotReviewStatus { TO_REVIEW(Set.of(SONARQUBE, SONARCLOUD)), SAFE(Set.of(SONARQUBE, SONARCLOUD)), FIXED(Set.of(SONARQUBE, SONARCLOUD)), ACKNOWLEDGED(Set.of(SONARQUBE)); private final Set allowedConnectionKinds; HotspotReviewStatus(Set allowedConnectionKinds) { this.allowedConnectionKinds = allowedConnectionKinds; } public boolean isReviewed() { return !equals(TO_REVIEW); } public boolean isResolved() { // ACKNOWLEDGED is considered as non-resolved because the hotspot is confirmed return equals(SAFE) || equals(FIXED); } public static HotspotReviewStatus fromStatusAndResolution(String status, @Nullable String resolution) { if ("REVIEWED".equals(status)) { if (resolution == null) { return HotspotReviewStatus.SAFE; } return switch (resolution) { case "SAFE" -> HotspotReviewStatus.SAFE; case "FIXED" -> HotspotReviewStatus.FIXED; case "ACKNOWLEDGED" -> HotspotReviewStatus.ACKNOWLEDGED; default -> HotspotReviewStatus.TO_REVIEW; }; } return HotspotReviewStatus.TO_REVIEW; } private boolean isAllowedOn(ConnectionKind kind) { return allowedConnectionKinds.contains(kind); } public static List allowedStatusesOn(ConnectionKind kind) { return Arrays.stream(HotspotReviewStatus.values()).filter(status -> status.isAllowedOn(kind)) .toList(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/IOExceptionUtils.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.io.IOException; import java.util.Queue; public class IOExceptionUtils { public static void tryAndCollectIOException(IORunnable runnable, Queue exceptions) { try { runnable.run(); } catch (IOException e) { exceptions.add(e); } } public static void throwFirstWithOtherSuppressed(Queue exceptions) throws IOException { if (!exceptions.isEmpty()) { var first = exceptions.poll(); exceptions.forEach(first::addSuppressed); throw first; } } public interface IORunnable { void run() throws IOException; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/ImpactSeverity.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum ImpactSeverity { INFO, LOW, MEDIUM, HIGH, BLOCKER; public static ImpactSeverity mapSeverity(String severity) { if ("BLOCKER".equals(severity) || "ImpactSeverity_BLOCKER".equals(severity)) { return ImpactSeverity.BLOCKER; } else if ("INFO".equals(severity) || "ImpactSeverity_INFO".equals(severity)) { return ImpactSeverity.INFO; } else { return ImpactSeverity.valueOf(severity); } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/IssueSeverity.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum IssueSeverity { INFO, MINOR, MAJOR, CRITICAL, BLOCKER } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/IssueStatus.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import javax.annotation.CheckForNull; /** * Represents Issue resolution status. Not the status of the issue itself. */ public enum IssueStatus { ACCEPT, WONT_FIX, FALSE_POSITIVE; @CheckForNull public static IssueStatus parse(String stringRepresentation) { return switch (stringRepresentation) { // ACCEPTED transition leads to WONTFIX status on server so we are not making difference between them. case "WONTFIX", "ACCEPT" -> IssueStatus.ACCEPT; case "FALSE-POSITIVE" -> IssueStatus.FALSE_POSITIVE; default -> null; }; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/KnownFinding.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.time.Instant; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class KnownFinding { private final UUID id; private final String serverKey; private final TextRangeWithHash textRangeWithHash; private final LineWithHash lineWithHash; private final String ruleKey; private final String message; private final Instant introductionDate; public KnownFinding(UUID id, @Nullable String serverKey, @Nullable TextRangeWithHash textRangeWithHash, @Nullable LineWithHash lineWithHash, String ruleKey, String message, Instant introductionDate) { this.id = id; this.serverKey = serverKey; this.textRangeWithHash = textRangeWithHash; this.lineWithHash = lineWithHash; this.ruleKey = ruleKey; this.message = message; this.introductionDate = introductionDate; } public UUID getId() { return id; } @CheckForNull public String getServerKey() { return serverKey; } @CheckForNull public TextRangeWithHash getTextRangeWithHash() { return textRangeWithHash; } @CheckForNull public LineWithHash getLineWithHash() { return lineWithHash; } public String getRuleKey() { return ruleKey; } public String getMessage() { return message; } public Instant getIntroductionDate() { return introductionDate; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/KnownFindingType.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum KnownFindingType { ISSUE, HOTSPOT } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/LineWithHash.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public class LineWithHash { private final int number; private final String hash; public LineWithHash(int number, String hash) { this.number = number; this.hash = hash; } public int getNumber() { return number; } public String getHash() { return hash; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/LocalOnlyIssue.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.nio.file.Path; import java.time.Instant; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class LocalOnlyIssue { private final UUID id; private final Path serverRelativePath; private final TextRangeWithHash textRangeWithHash; private final LineWithHash lineWithHash; private final String ruleKey; private final String message; private LocalOnlyIssueResolution resolution; /** * @param resolution is null when the issue is not resolved */ public LocalOnlyIssue(UUID id, Path serverRelativePath, @Nullable TextRangeWithHash textRangeWithHash, @Nullable LineWithHash lineWithHash, String ruleKey, String message, @Nullable LocalOnlyIssueResolution resolution) { this.id = id; this.serverRelativePath = serverRelativePath; this.textRangeWithHash = textRangeWithHash; this.lineWithHash = lineWithHash; this.ruleKey = ruleKey; this.message = message; this.resolution = resolution; } public UUID getId() { return id; } public Path getServerRelativePath() { return serverRelativePath; } @CheckForNull public TextRangeWithHash getTextRangeWithHash() { return textRangeWithHash; } @CheckForNull public LineWithHash getLineWithHash() { return lineWithHash; } public String getRuleKey() { return ruleKey; } public String getMessage() { return message; } @CheckForNull public LocalOnlyIssueResolution getResolution() { return resolution; } public void resolve(IssueStatus newStatus) { resolution = new LocalOnlyIssueResolution(newStatus, Instant.now(), null); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/LocalOnlyIssueResolution.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.time.Instant; import javax.annotation.CheckForNull; import javax.annotation.Nullable; public class LocalOnlyIssueResolution { private final IssueStatus resolutionStatus; private final Instant resolutionDate; private String comment; public LocalOnlyIssueResolution(IssueStatus status, Instant resolutionDate, @Nullable String comment) { this.resolutionStatus = status; this.resolutionDate = resolutionDate; this.comment = comment; } public IssueStatus getStatus() { return resolutionStatus; } public Instant getResolutionDate() { return resolutionDate; } @CheckForNull public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/MultiFileBlameResult.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.apache.commons.io.FilenameUtils; import org.sonarsource.sonarlint.core.commons.util.git.BlameResult; import static java.util.Objects.isNull; public class MultiFileBlameResult { private final Map blameResultPerFile; private final Path gitRepoRelativeProjectBaseDir; public MultiFileBlameResult(Map blameResultPerFile, Path gitRepoRelativeProjectBaseDir) { this.blameResultPerFile = blameResultPerFile; this.gitRepoRelativeProjectBaseDir = gitRepoRelativeProjectBaseDir; } public static MultiFileBlameResult empty(Path gitRepoRelativeProjectBaseDir) { return new MultiFileBlameResult(Map.of(), gitRepoRelativeProjectBaseDir); } /** * @param projectDirRelativeFilePath A path relative to the Git repository root * @param lineNumbers Line numbers for which to check the latest change date. Numbering starts from `1`! * @return The latest changed date or an empty optional if the date couldn't be determined or any of the lines is modified */ public Optional getLatestChangeDateForLinesInFile(Path projectDirRelativeFilePath, Collection lineNumbers) { validateLineNumbersArgument(lineNumbers); return Optional.of(projectDirRelativeFilePath.toString()) .map(gitRepoRelativeProjectBaseDir::resolve) .map(Path::toString) .map(FilenameUtils::separatorsToUnix) .map(blameResultPerFile::get) .map(fileBlame -> getTheLatestChange(fileBlame, lineNumbers)); } private static Instant getTheLatestChange(BlameResult blameForFile, Collection lineNumbers) { Instant latestDate = null; for (var lineNumber : lineNumbers) { if (lineNumber > blameForFile.lineCommitDates().size()) { continue; } var dateForLine = blameForFile.lineCommitDates().get(lineNumber - 1); if (isLineModified(dateForLine)) { return null; } latestDate = isNull(latestDate) || latestDate.isBefore(dateForLine) ? dateForLine : latestDate; } return latestDate; } private static void validateLineNumbersArgument(Collection lineNumbers) { if (lineNumbers.stream().anyMatch(i -> i < 1)) { throw new IllegalArgumentException("Line numbers must be greater than 0. The numbering starts from 1 (i.e. the " + "first line of a file should be `1`)"); } } private static boolean isLineModified(@Nullable Instant dateForLine) { return dateForLine == null; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/NewCodeDefinition.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import javax.annotation.CheckForNull; import javax.annotation.Nullable; public interface NewCodeDefinition { String DATETIME_FORMAT = "MM/dd/yyyy HH:mm:ss"; DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); NewCodeMode getMode(); boolean isOnNewCode(long creationDate); default boolean isOnNewCode(Instant introductionDate) { return isOnNewCode(introductionDate.toEpochMilli()); } boolean isSupported(); static String formatEpochToDate(long epoch) { return ZonedDateTime.ofInstant(Instant.ofEpochMilli(epoch), ZoneId.systemDefault()).format(DATETIME_FORMATTER); } static NewCodeDefinition withAlwaysNew() { return new NewCodeAlwaysNew(); } static NewCodeDefinition withExactNumberOfDays(int days) { return new NewCodeExactNumberOfDays(days); } /** * @param days the theoretical number of days * @param thresholdDate the actual date in the past that serves for the comparison. Can be different from the number of days as it is updated after analysis on the server side */ static NewCodeDefinition withNumberOfDaysWithDate(int days, long thresholdDate) { return new NewCodeNumberOfDaysWithDate(days, thresholdDate); } static NewCodeDefinition withPreviousVersion(long thresholdDate, @Nullable String version) { return new NewCodePreviousVersion(thresholdDate, version); } static NewCodeDefinition withReferenceBranch(String referenceBranch) { return new NewCodeReferenceBranch(referenceBranch); } static NewCodeDefinition withSpecificAnalysis(long thresholdDate) { return new NewCodeSpecificAnalysis(thresholdDate); } Instant getThresholdDate(); abstract class NewCodeDefinitionWithDate implements NewCodeDefinition { protected final long thresholdDate; protected NewCodeDefinitionWithDate(long thresholdDate) { this.thresholdDate = thresholdDate; } public boolean isOnNewCode(long creationDate) { return creationDate > thresholdDate; } public boolean isSupported() { return true; } public Instant getThresholdDate() { return Instant.ofEpochMilli(thresholdDate); } } class NewCodeExactNumberOfDays implements NewCodeDefinition { private final int days; public NewCodeExactNumberOfDays(int days) { this.days = days; } @Override public NewCodeMode getMode() { return NewCodeMode.NUMBER_OF_DAYS; } @Override public boolean isOnNewCode(long creationDate) { return creationDate > Instant.now().minus(days, ChronoUnit.DAYS).toEpochMilli(); } @Override public boolean isSupported() { return true; } @Override public Instant getThresholdDate() { return Instant.now().minus(days, ChronoUnit.DAYS); } // Text used by IDEs in the UI. Communicate changes to IDE squad prior to changing the wording. @Override public String toString() { return String.format("From last %s days", days); } } class NewCodeNumberOfDaysWithDate extends NewCodeDefinitionWithDate { Integer days; private NewCodeNumberOfDaysWithDate(Integer days, long thresholdDate) { super(thresholdDate); this.days = days; } // Text used by IDEs in the UI. Communicate changes to IDE squad prior to changing the wording. @Override public String toString() { return String.format("From last %s days", days); } @Override public NewCodeMode getMode() { return NewCodeMode.NUMBER_OF_DAYS; } public Integer getDays() { return days; } } class NewCodePreviousVersion extends NewCodeDefinitionWithDate { private final String version; private NewCodePreviousVersion(long thresholdDate, @Nullable String version) { super(thresholdDate); this.version = version; } // Text used by IDEs in the UI. Communicate changes to IDE squad prior to changing the wording. @Override public String toString() { var versionQualifier = (version == null) ? formatEpochToDate(this.thresholdDate) : ("version " + version); return String.format("Since %s", versionQualifier); } @Override public NewCodeMode getMode() { return NewCodeMode.PREVIOUS_VERSION; } @CheckForNull public String getVersion() { return version; } } class NewCodeSpecificAnalysis extends NewCodeDefinitionWithDate { private NewCodeSpecificAnalysis(long thresholdDate) { super(thresholdDate); } // Text used by IDEs in the UI. Communicate changes to IDE squad prior to changing the wording. @Override public String toString() { return String.format("Since analysis from %s", formatEpochToDate(this.thresholdDate)); } @Override public NewCodeMode getMode() { return NewCodeMode.SPECIFIC_ANALYSIS; } } class NewCodeReferenceBranch implements NewCodeDefinition { private final String branchName; private NewCodeReferenceBranch(String branchName) { this.branchName = branchName; } @Override public NewCodeMode getMode() { return NewCodeMode.REFERENCE_BRANCH; } @Override public boolean isOnNewCode(long creationDate) { return true; } @Override public boolean isSupported() { return false; } public String getBranchName() { return branchName; } @Override public Instant getThresholdDate() { // instead of Long.MAX_VALUE it's set for Instant.now() in case it will be used for git blame limit return Instant.now(); } // Text used by IDEs in the UI. Communicate changes to IDE squad prior to changing the wording. @Override public String toString() { return "Current new code definition (reference branch) is not supported"; } } class NewCodeAlwaysNew implements NewCodeDefinition { private NewCodeAlwaysNew() { // NOP } @Override public NewCodeMode getMode() { throw new UnsupportedOperationException("Mode shouldn't be called for this new code definition"); } @Override public boolean isOnNewCode(long creationDate) { return true; } @Override public Instant getThresholdDate() { // instead of 0L it's set for Instant.now() in case it will be used for git blame limit (which shouldn't normally happen) return Instant.now(); } @Override public boolean isSupported() { return true; } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/NewCodeMode.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum NewCodeMode { REFERENCE_BRANCH, NUMBER_OF_DAYS, PREVIOUS_VERSION, SPECIFIC_ANALYSIS } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/RuleKey.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.Objects; import javax.annotation.concurrent.Immutable; @Immutable public class RuleKey { private static final char SEPARATOR = ':'; private final String repository; private final String rule; public RuleKey(String repository, String rule) { this.repository = repository; this.rule = rule; } public String repository() { return repository; } public String rule() { return rule; } public static RuleKey parse(String s) { var separatorIndex = s.indexOf(SEPARATOR); if (separatorIndex < 0) { throw new IllegalArgumentException("Invalid rule key: " + s); } var key = s.substring(0, separatorIndex); var repo = s.substring(separatorIndex + 1); return new RuleKey(key, repo); } @Override public String toString() { return repository + SEPARATOR + rule; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } var ruleKey = (RuleKey) o; return Objects.equals(repository, ruleKey.repository) && Objects.equals(rule, ruleKey.rule); } @Override public int hashCode() { return Objects.hash(repository, rule); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/RuleType.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum RuleType { CODE_SMELL, BUG, VULNERABILITY, SECURITY_HOTSPOT } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/SoftwareQuality.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum SoftwareQuality { MAINTAINABILITY, RELIABILITY, SECURITY } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/SonarLintCoreVersion.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.io.IOException; import java.nio.charset.StandardCharsets; public class SonarLintCoreVersion { private SonarLintCoreVersion() { } public static String get() { String version; var packageInfo = SonarLintCoreVersion.class.getPackage(); if (packageInfo != null && packageInfo.getImplementationVersion() != null) { version = packageInfo.getImplementationVersion(); } else { version = getLibraryVersion(); } return version; } public static String getLibraryVersion() { var version = "unknown"; var resource = SonarLintCoreVersion.class.getResourceAsStream("/sl_core_version.txt"); if (resource != null) { try { version = new String(resource.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { return version; } } return version; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/SonarLintException.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import javax.annotation.Nullable; public class SonarLintException extends RuntimeException { public SonarLintException() { super(); } public SonarLintException(String msg) { super(msg); } public SonarLintException(String msg, @Nullable Throwable cause) { super(msg, cause); } public SonarLintException(String msg, @Nullable Throwable cause, boolean withStackTrace) { super(msg, cause, !withStackTrace, withStackTrace); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/SonarLintGitIgnore.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.nio.file.Path; import org.eclipse.jgit.ignore.IgnoreNode; public class SonarLintGitIgnore { private final IgnoreNode ignoreNode; public SonarLintGitIgnore(IgnoreNode ignoreNode) { this.ignoreNode = ignoreNode; } public boolean isIgnored(Path clientRelativeFilePath) { var normalizedUnixPath = clientRelativeFilePath.toString().replace("\\", "/"); var rules = ignoreNode.getRules(); // Parse rules in the reverse order that they were read because later rules have higher priority for (var i = rules.size() - 1; i > -1; i--) { var rule = rules.get(i); if (rule.isMatch(normalizedUnixPath, false)) { return rule.getResult(); } } return false; } public boolean isFileIgnored(Path clientFileRelativePath) { return isIgnored(clientFileRelativePath); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/SonarLintUserHome.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.nio.file.Path; import java.nio.file.Paths; import javax.annotation.Nullable; public class SonarLintUserHome { public static final String SONARLINT_USER_HOME_ENV = "SONARLINT_USER_HOME"; private SonarLintUserHome() { // utility class, forbidden constructor } public static Path get() { return home(System.getenv(SONARLINT_USER_HOME_ENV)); } static Path home(@Nullable String slHome) { if (slHome != null) { return Paths.get(slHome); } return Paths.get(System.getProperty("user.home")).resolve(".sonarlint"); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/Transition.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public enum Transition { ACCEPT("accept"), WONT_FIX("wontfix"), FALSE_POSITIVE("falsepositive"), REOPEN("reopen"); private final String status; Transition(String status) { this.status = status; } public String getStatus() { return status; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/Version.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.Arrays; public class Version implements Comparable { private final String name; private final String nameWithoutQualifier; private final int[] numbers; private final String qualifier; private Version(String version) { this.name = version.trim(); var qualifierPosition = name.indexOf("-"); if (qualifierPosition != -1) { this.qualifier = name.substring(qualifierPosition + 1); this.nameWithoutQualifier = name.substring(0, qualifierPosition); } else { this.qualifier = ""; this.nameWithoutQualifier = this.name; } final var split = this.nameWithoutQualifier.split("\\."); numbers = new int[split.length]; for (var i = 0; i < split.length; i++) { numbers[i] = Integer.parseInt(split[i]); } } private Version(String name, String nameWithoutQualifier, int[] numbers, String qualifier) { this.name = name; this.nameWithoutQualifier = nameWithoutQualifier; this.numbers = Arrays.copyOf(numbers, numbers.length); this.qualifier = qualifier; } public int getMajor() { return numbers.length > 0 ? numbers[0] : 0; } public int getMinor() { return numbers.length > 1 ? numbers[1] : 0; } public int getPatch() { return numbers.length > 2 ? numbers[2] : 0; } public int getBuild() { return numbers.length > 3 ? numbers[3] : 0; } public String getName() { return name; } public String getQualifier() { return qualifier; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Version other)) { return false; } return getMajor() == other.getMajor() && getMinor() == other.getMinor() && getPatch() == other.getPatch() && getBuild() == other.getBuild() && qualifier.equals(other.qualifier); } @Override public int hashCode() { var result = Integer.hashCode(getMajor()); result = 31 * result + Integer.hashCode(getMinor()); result = 31 * result + Integer.hashCode(getPatch()); result = 31 * result + Integer.hashCode(getBuild()); result = 31 * result + qualifier.hashCode(); return result; } @Override public int compareTo(Version other) { var c = compareToIgnoreQualifier(other); if (c == 0) { if ("".equals(qualifier)) { c = "".equals(other.qualifier) ? 0 : 1; } else if ("".equals(other.qualifier)) { c = -1; } else { c = qualifier.compareTo(other.qualifier); } } return c; } public int compareToIgnoreQualifier(Version other) { var maxNumbers = Math.max(numbers.length, other.numbers.length); var myNumbers = Arrays.copyOf(numbers, maxNumbers); var otherNumbers = Arrays.copyOf(other.numbers, maxNumbers); for (var i = 0; i < maxNumbers; i++) { var compare = Integer.compare(myNumbers[i], otherNumbers[i]); if (compare != 0) { return compare; } } return 0; } @Override public String toString() { return name; } public static Version create(String version) { return new Version(version); } public Version removeQualifier() { return new Version(nameWithoutQualifier, nameWithoutQualifier, numbers, ""); } public boolean satisfiesMinRequirement(Version minRequirement) { return this.compareToIgnoreQualifier(minRequirement) >= 0; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/VulnerabilityProbability.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.Arrays; import java.util.Optional; import javax.annotation.Nullable; public enum VulnerabilityProbability { HIGH(3), MEDIUM(2), LOW(1); private final int score; VulnerabilityProbability(int index) { this.score = index; } public int getScore() { return score; } public static Optional byScore(@Nullable Integer score) { if (score == null) { return Optional.empty(); } return Arrays.stream(values()) .filter(t -> t.score == score) .findFirst(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/SonarLanguage.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.api; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; public enum SonarLanguage { ABAP("abap", SonarPlugin.ABAP, "Abap", new String[]{".abap", ".ab4", ".flow", ".asprog"}, "sonar.abap.file.suffixes"), APEX("apex", SonarPlugin.APEX, "Apex", new String[]{".cls", ".trigger"}, "sonar.apex.file.suffixes"), C("c", SonarPlugin.C_FAMILY, "C", new String[]{".c", ".h"}, "sonar.c.file.suffixes"), CPP("cpp", SonarPlugin.C_FAMILY, "C++", new String[]{".cc", ".cpp", ".cxx", ".c++", ".hh", ".hpp", ".hxx", ".h++", ".ipp"}, "sonar.cpp.file.suffixes"), CS("cs", SonarPlugin.CS_OSS, "C#", new String[]{".cs", ".razor"}, "sonar.cs.file.suffixes"), CSS("css", SonarPlugin.JS, "CSS", new String[]{".css", ".less", ".scss"}, "sonar.css.file.suffixes"), OBJC("objc", SonarPlugin.C_FAMILY, "Objective-C", new String[]{".m"}, "sonar.objc.file.suffixes"), COBOL("cobol", SonarPlugin.COBOL, "COBOL", new String[0], "sonar.cobol.file.suffixes"), HTML("web", SonarPlugin.WEB, "HTML", new String[]{".html", ".xhtml", ".cshtml", ".vbhtml", ".aspx", ".ascx", ".rhtml", ".erb", ".shtm", ".shtml"}, "sonar.html.file.suffixes"), IPYTHON("ipynb", SonarPlugin.PYTHON, "IPython Notebook", new String[]{".ipynb"}, "sonar.ipython.file.suffixes"), JAVA("java", SonarPlugin.JAVA, "Java", new String[]{".java", ".jav"}, "sonar.java.file.suffixes"), JCL("jcl", SonarPlugin.JCL, "JCL", new String[]{".jcl"}, "sonar.jcl.file.suffixes"), JS("js", SonarPlugin.JS, "JavaScript", new String[]{".js", ".jsx", ".vue"}, "sonar.javascript.file.suffixes"), KOTLIN("kotlin", SonarPlugin.KOTLIN, "Kotlin", new String[]{".kt", ".kts"}, "sonar.kotlin.file.suffixes"), PHP("php", SonarPlugin.PHP, "PHP", new String[]{"php", "php3", "php4", "php5", "phtml", "inc"}, "sonar.php.file.suffixes"), PLI("pli", SonarPlugin.PLI, "PL/I", new String[]{".pli"}, "sonar.pli.file.suffixes"), PLSQL("plsql", SonarPlugin.PLSQL, "PL/SQL", new String[]{".sql", ".pks", ".pkb"}, "sonar.plsql.file.suffixes"), PYTHON("py", SonarPlugin.PYTHON, "Python", new String[]{".py"}, "sonar.python.file.suffixes"), RPG("rpg", SonarPlugin.RPG, "RPG", new String[]{".rpg", ".rpgle"}, "sonar.rpg.file.suffixes"), RUBY("ruby", SonarPlugin.RUBY, "Ruby", new String[]{".rb"}, "sonar.ruby.file.suffixes"), SCALA("scala", SonarPlugin.SCALA, "Scala", new String[]{".scala"}, "sonar.scala.file.suffixes"), SECRETS("secrets", SonarPlugin.TEXT, "Secrets", new String[0], "sonar.secrets.file.suffixes"), TEXT("text", SonarPlugin.TEXT, "Text", new String[0], "sonar.text.file.suffixes"), SWIFT("swift", SonarPlugin.SWIFT, "Swift", new String[]{".swift"}, "sonar.swift.file.suffixes"), TSQL("tsql", SonarPlugin.TSQL, "T-SQL", new String[]{".tsql"}, "sonar.tsql.file.suffixes"), TS("ts", SonarPlugin.JS, "TypeScript", new String[]{".ts", ".tsx"}, "sonar.typescript.file.suffixes"), JSP("jsp", SonarPlugin.WEB, "JSP", new String[]{".jsp", ".jspf", ".jspx"}, "sonar.jsp.file.suffixes"), VBNET("vbnet", SonarPlugin.VBNET_OSS, "VB.NET", new String[]{".vb"}, "sonar.vbnet.file.suffixes"), XML("xml", SonarPlugin.XML, "XML", new String[]{".xml", ".xsd", ".xsl"}, "sonar.xml.file.suffixes"), YAML("yaml", SonarPlugin.JS, "YAML", new String[]{".yml", "yaml"}, Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), JSON("json", SonarPlugin.JS, "JSON", new String[]{".json"}, Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), GO("go", SonarPlugin.GO, "Go", new String[]{".go"}, "sonar.go.file.suffixes"), CLOUDFORMATION("cloudformation", SonarPlugin.IAC, "CloudFormation", new String[0], Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), DOCKER("docker", SonarPlugin.IAC, "Docker", new String[0], Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), KUBERNETES("kubernetes", SonarPlugin.IAC, "Kubernetes", new String[0], Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), TERRAFORM("terraform", SonarPlugin.IAC, "Terraform", new String[]{".tf"}, "sonar.terraform.file.suffixes"), AZURERESOURCEMANAGER("azureresourcemanager", SonarPlugin.IAC, "Azure Resource Manager", new String[]{".bicep"}, Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), ANSIBLE("ansible", SonarPlugin.IAC, "Ansible", new String[0], Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE), GITHUBACTIONS("githubactions", SonarPlugin.IAC, "GitHub Actions", new String[0], Constants.NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE); private final String sonarLanguageKey; /** * The Sonar Plugin declaring this language */ private final SonarPlugin plugin; private final String name; private final String[] defaultFileSuffixes; private final String fileSuffixesPropKey; private static final Map mMap = Collections.unmodifiableMap(initializeMapping()); private static Map initializeMapping() { Map mMap = new HashMap<>(); for (SonarLanguage l : SonarLanguage.values()) { mMap.put(l.sonarLanguageKey, l); } return mMap; } SonarLanguage(String sonarLanguageKey, SonarPlugin plugin, String name, String[] defaultFileSuffixes, String fileSuffixesPropKey) { this.sonarLanguageKey = sonarLanguageKey; this.plugin = plugin; this.name = name; this.defaultFileSuffixes = defaultFileSuffixes; this.fileSuffixesPropKey = fileSuffixesPropKey; } public String getSonarLanguageKey() { return sonarLanguageKey; } public SonarPlugin getPlugin() { return plugin; } public String getName() { return name; } public String[] getDefaultFileSuffixes() { return defaultFileSuffixes; } public String getFileSuffixesPropKey() { return fileSuffixesPropKey; } public boolean shouldSyncInConnectedMode() { return !equals(SonarLanguage.IPYTHON); } public static Optional getLanguageByLanguageKey(String languageKey) { var languages = Stream.of(values()).filter(l -> l.getSonarLanguageKey().equals(languageKey)).collect(Collectors.toCollection(ArrayList::new)); return languages.isEmpty() ? Optional.empty() : Optional.of(languages.get(0)); } public static Optional forKey(String languageKey) { return Optional.ofNullable(mMap.get(languageKey)); } public static class Constants { private static final String NO_PUBLIC_PROPERTY_PROVIDED_FOR_THIS_LANGUAGE = ""; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/TextRange.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.api; import java.util.Objects; public class TextRange { private final int startLine; private final int startLineOffset; private final int endLine; private final int endLineOffset; public TextRange(int startLine, int startLineOffset, int endLine, int endLineOffset) { this.startLine = startLine; this.startLineOffset = startLineOffset; this.endLine = endLine; this.endLineOffset = endLineOffset; } public int getStartLine() { return startLine; } public int getStartLineOffset() { return startLineOffset; } public int getEndLine() { return endLine; } public int getEndLineOffset() { return endLineOffset; } @Override public int hashCode() { return Objects.hash(endLine, endLineOffset, startLine, startLineOffset); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof TextRange other)) { return false; } return endLine == other.endLine && endLineOffset == other.endLineOffset && startLine == other.startLine && startLineOffset == other.startLineOffset; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/TextRangeWithHash.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.api; import java.util.Objects; public class TextRangeWithHash extends TextRange { private final String hash; public TextRangeWithHash(int startLine, int startLineOffset, int endLine, int endLineOffset, String hash) { super(startLine, startLineOffset, endLine, endLineOffset); this.hash = hash; } public String getHash() { return hash; } @Override public int hashCode() { final var prime = 31; int result = super.hashCode(); result = prime * result + Objects.hash(hash); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!super.equals(obj)) { return false; } if (!(obj instanceof TextRangeWithHash other)) { return false; } return Objects.equals(hash, other.hash); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/progress/CanceledException.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.api.progress; import org.sonarsource.sonarlint.core.commons.SonarLintException; public class CanceledException extends SonarLintException { } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/api/progress/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.api.progress; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/dogfood/DogfoodEnvironmentDetectionService.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.dogfood; import org.apache.commons.lang3.SystemUtils; public class DogfoodEnvironmentDetectionService { public static final String SONARSOURCE_DOGFOODING_ENV_VAR_KEY = "SONARSOURCE_DOGFOODING"; public boolean isDogfoodEnvironment() { return "1".equals(SystemUtils.getEnvironmentVariable(SONARSOURCE_DOGFOODING_ENV_VAR_KEY, "0")); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/dogfood/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.dogfood; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/FormattingTuple.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import javax.annotation.CheckForNull; import javax.annotation.Nullable; class FormattingTuple { private final String message; private final Throwable throwable; public FormattingTuple(@Nullable String message) { this(message, null); } public FormattingTuple(@Nullable String message, @Nullable Throwable throwable) { this.message = message; this.throwable = throwable; } @CheckForNull public String getMessage() { return message; } @CheckForNull public Throwable getThrowable() { return throwable; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/LogOutput.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import java.io.PrintWriter; import java.io.StringWriter; import javax.annotation.Nullable; /** * Allow to redirect SonarLint logs to a custom output on client side */ public interface LogOutput { /** * @deprecated please implement {@link #log(String, Level, String)} instead */ @Deprecated(since = "10.0") default void log(String formattedMessage, Level level) { log(formattedMessage, level, null); } default void log(@Nullable String formattedMessage, Level level, @Nullable String stacktrace) { if (formattedMessage != null) { log(formattedMessage, level); } if (stacktrace != null) { log(stacktrace, level); } } enum Level { OFF, ERROR, WARN, INFO, DEBUG, TRACE; public boolean isMoreVerboseOrEqual(Level targetLevel) { return this.ordinal() >= targetLevel.ordinal(); } } static String stackTraceToString(Throwable t) { var stringWriter = new StringWriter(); var printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); return stringWriter.toString(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/MessageFormatter.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; // Inspired by https://github.com/qos-ch/slf4j/blob/98f1f2f46533eba4945dda995225cf3c4017a075/slf4j-api/src/main/java/org/slf4j/helpers/MessageFormatter.java // contributors: lizongbo: proposed special treatment of array parameter values // Joern Huxhorn: pointed out double[] omission, suggested deep array copy /** * Formats messages according to very simple substitution rules. Substitutions * can be made 1, 2 or more arguments. * *

* For example, * *

 * MessageFormatter.format("Hi {}.", "there")
 * 
* * will return the string "Hi there.". *

* The {} pair is called the formatting anchor. It serves to designate * the location where arguments need to be substituted within the message * pattern. *

* In case your message contains the '{' or the '}' character, you do not have * to do anything special unless the '}' character immediately follows '{'. For * example, * *

 * MessageFormatter.format("Set {1,2,3} is not equal to {}.", "1,2");
 * 
* * will return the string "Set {1,2,3} is not equal to 1,2.". * *

* If for whatever reason you need to place the string "{}" in the message * without its formatting anchor meaning, then you need to escape the * '{' character with '\', that is the backslash character. Only the '{' * character should be escaped. There is no need to escape the '}' character. * For example, * *

 * MessageFormatter.format("Set \\{} is not equal to {}.", "1,2");
 * 
* * will return the string "Set {} is not equal to 1,2.". * *

* The escaping behavior just described can be overridden by escaping the escape * character '\'. Calling * *

 * MessageFormatter.format("File name is C:\\\\{}.", "file.zip");
 * 
* * will return the string "File name is C:\file.zip". * *

* The formatting conventions are different than those of {@link MessageFormat} * which ships with the Java platform. This is justified by the fact that * SLF4J's implementation is 10 times faster than that of {@link MessageFormat}. * This local performance difference is both measurable and significant in the * larger context of the complete logging processing chain. * *

* See also {@link #format(String, Object)}, * {@link #format(String, Object, Object)} and * {@link #arrayFormat(String, Object[])} methods for more details. * * @author Ceki Gülcü * @author Joern Huxhorn */ final class MessageFormatter { static final char DELIM_START = '{'; static final char DELIM_STOP = '}'; static final String DELIM_STR = "{}"; private static final char ESCAPE_CHAR = '\\'; private MessageFormatter() { } /** * Performs single argument substitution for the 'messagePattern' passed as * parameter. *

* For example, * *

   * MessageFormatter.format("Hi {}.", "there");
   * 
* * will return the string "Hi there.". *

* * @param messagePattern * The message pattern which will be parsed and formatted * @param arg * The argument to be substituted in place of the formatting anchor * @return The formatted message */ public static FormattingTuple format(String messagePattern, Object arg) { return arrayFormat(messagePattern, new Object[] {arg}); } /** * * Performs a two argument substitution for the 'messagePattern' passed as * parameter. *

* For example, * *

   * MessageFormatter.format("Hi {}. My name is {}.", "Alice", "Bob");
   * 
* * will return the string "Hi Alice. My name is Bob.". * * @param messagePattern * The message pattern which will be parsed and formatted * @param arg1 * The argument to be substituted in place of the first formatting * anchor * @param arg2 * The argument to be substituted in place of the second formatting * anchor * @return The formatted message */ public static FormattingTuple format(final String messagePattern, Object arg1, Object arg2) { return arrayFormat(messagePattern, new Object[] {arg1, arg2}); } public static FormattingTuple arrayFormat(final String messagePattern, final Object[] argArray) { var throwableCandidate = MessageFormatter.getThrowableCandidate(argArray); var args = argArray; if (throwableCandidate != null) { args = MessageFormatter.trimmedCopy(argArray); } return arrayFormat(messagePattern, args, throwableCandidate); } public static FormattingTuple arrayFormat(@Nullable final String messagePattern, @Nullable final Object[] argArray, @Nullable Throwable throwable) { if (messagePattern == null) { return new FormattingTuple(null, throwable); } if (argArray == null) { return new FormattingTuple(messagePattern); } var i = 0; int j; // use string builder for better multicore performance var sbuf = new StringBuilder(messagePattern.length() + 50); int L; for (L = 0; L < argArray.length; L++) { j = messagePattern.indexOf(DELIM_STR, i); if (j == -1) { // no more variables if (i == 0) { // this is a simple string return new FormattingTuple(messagePattern, throwable); } else { // add the tail string which contains no variables and return // the result. sbuf.append(messagePattern, i, messagePattern.length()); return new FormattingTuple(sbuf.toString(), throwable); } } else { if (isEscapedDelimiter(messagePattern, j)) { if (!isDoubleEscaped(messagePattern, j)) { L--; // DELIM_START was escaped, thus should not be incremented sbuf.append(messagePattern, i, j - 1); sbuf.append(DELIM_START); i = j + 1; } else { // The escape character preceding the delimiter start is // itself escaped: "abc x:\\{}" // we have to consume one backward slash sbuf.append(messagePattern, i, j - 1); deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); i = j + 2; } } else { // normal case sbuf.append(messagePattern, i, j); deeplyAppendParameter(sbuf, argArray[L], new HashMap<>()); i = j + 2; } } } // append the characters following the last {} pair. sbuf.append(messagePattern, i, messagePattern.length()); return new FormattingTuple(sbuf.toString(), throwable); } static boolean isEscapedDelimiter(String messagePattern, int delimiterStartIndex) { if (delimiterStartIndex == 0) { return false; } var potentialEscape = messagePattern.charAt(delimiterStartIndex - 1); return potentialEscape == ESCAPE_CHAR; } static boolean isDoubleEscaped(String messagePattern, int delimeterStartIndex) { return delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR; } // special treatment of array values was suggested by 'lizongbo' private static void deeplyAppendParameter(StringBuilder sbuf, @Nullable Object o, Map seenMap) { if (o == null) { sbuf.append("null"); return; } if (!o.getClass().isArray()) { safeObjectAppend(sbuf, o); } else { // check for primitive array types because they // unfortunately cannot be cast to Object[] switch (o) { case boolean[] booleans -> booleanArrayAppend(sbuf, booleans); case byte[] bytes -> byteArrayAppend(sbuf, bytes); case char[] chars -> charArrayAppend(sbuf, chars); case short[] shorts -> shortArrayAppend(sbuf, shorts); case int[] ints -> intArrayAppend(sbuf, ints); case long[] longs -> longArrayAppend(sbuf, longs); case float[] floats -> floatArrayAppend(sbuf, floats); case double[] doubles -> doubleArrayAppend(sbuf, doubles); default -> objectArrayAppend(sbuf, (Object[]) o, seenMap); } } } private static void safeObjectAppend(StringBuilder sbuf, Object o) { try { var oAsString = o.toString(); sbuf.append(oAsString); } catch (Exception e) { sbuf.append("[FAILED toString()]"); } } private static void objectArrayAppend(StringBuilder sbuf, Object[] a, Map seenMap) { sbuf.append('['); if (!seenMap.containsKey(a)) { seenMap.put(a, null); final var len = a.length; for (var i = 0; i < len; i++) { deeplyAppendParameter(sbuf, a[i], seenMap); if (i != len - 1) { sbuf.append(", "); } } // allow repeats in siblings seenMap.remove(a); } else { sbuf.append("..."); } sbuf.append(']'); } private static void booleanArrayAppend(StringBuilder sbuf, boolean[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void byteArrayAppend(StringBuilder sbuf, byte[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void charArrayAppend(StringBuilder sbuf, char[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void shortArrayAppend(StringBuilder sbuf, short[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void intArrayAppend(StringBuilder sbuf, int[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void longArrayAppend(StringBuilder sbuf, long[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void floatArrayAppend(StringBuilder sbuf, float[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } private static void doubleArrayAppend(StringBuilder sbuf, double[] a) { sbuf.append('['); final var len = a.length; for (var i = 0; i < len; i++) { sbuf.append(a[i]); if (i != len - 1) { sbuf.append(", "); } } sbuf.append(']'); } /** * Helper method to determine if an {@link Object} array contains a {@link Throwable} as last element * * @param argArray * The arguments off which we want to know if it contains a {@link Throwable} as last element * @return if the last {@link Object} in argArray is a {@link Throwable} this method will return it, * otherwise it returns null */ public static Throwable getThrowableCandidate(final Object[] argArray) { return NormalizedParameters.getThrowableCandidate(argArray); } /** * Helper method to get all but the last element of an array * * @param argArray * The arguments from which we want to remove the last element * * @return a copy of the array without the last element */ public static Object[] trimmedCopy(final Object[] argArray) { return NormalizedParameters.trimmedCopy(argArray); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/NormalizedParameters.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import javax.annotation.Nullable; /** * Holds normalized calling call parameters. * * Includes utility methods such as {@link #normalize(String, Object[], Throwable)} to help the normalization of parameters. * * @author ceki * @since 2.0 */ class NormalizedParameters { private NormalizedParameters() { } /** * Helper method to determine if an {@link Object} array contains a * {@link Throwable} as last element * * @param argArray The arguments off which we want to know if it contains a * {@link Throwable} as last element * @return if the last {@link Object} in argArray is a {@link Throwable} this * method will return it, otherwise it returns null */ public static Throwable getThrowableCandidate(@Nullable final Object[] argArray) { if (argArray == null || argArray.length == 0) { return null; } final var lastEntry = argArray[argArray.length - 1]; if (lastEntry instanceof Throwable throwable) { return throwable; } return null; } /** * Helper method to get all but the last element of an array * * @param argArray The arguments from which we want to remove the last element * * @return a copy of the array without the last element */ public static Object[] trimmedCopy(@Nullable final Object[] argArray) { if (argArray == null || argArray.length == 0) { throw new IllegalStateException("non-sensical empty or null argument array"); } final var trimmedLen = argArray.length - 1; var trimmed = new Object[trimmedLen]; if (trimmedLen > 0) { System.arraycopy(argArray, 0, trimmed, 0, trimmedLen); } return trimmed; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/SonarLintLogger.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import io.sentry.Sentry; import io.sentry.SentryLogLevel; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; /** * This is the logging facade to be used in SonarLint core. */ public class SonarLintLogger { private static final SonarLintLogger logger = new SonarLintLogger(); private Level currentLevel = Level.OFF; public static SonarLintLogger get() { return logger; } private final InheritableThreadLocal target = new InheritableThreadLocal<>(); SonarLintLogger() { // singleton class } public void setTarget(@Nullable LogOutput target) { this.target.set(target); } /** * In some cases, the log output is not properly inherited by the "child" threads (especially when using shared thread pools). * We have to copy the log output manually, in a similar way to https://logback.qos.ch/manual/mdc.html#managedThreads */ @CheckForNull public LogOutput getTargetForCopy() { return this.target.get(); } public void trace(String msg) { log(msg, Level.TRACE, (Throwable) null); } public void trace(String msg, @Nullable Object arg) { doLogExtractingThrowable(Level.TRACE, msg, new Object[]{arg}); } public void trace(String msg, @Nullable Object arg1, @Nullable Object arg2) { doLogExtractingThrowable(Level.TRACE, msg, new Object[]{arg1, arg2}); } public void trace(String msg, Object... args) { doLogExtractingThrowable(Level.TRACE, msg, args); } public void debug(String msg) { log(msg, Level.DEBUG, (Throwable) null); } public void debug(String msg, @Nullable Object arg) { doLogExtractingThrowable(Level.DEBUG, msg, new Object[]{arg}); } public void debug(String msg, @Nullable Object arg1, @Nullable Object arg2) { doLogExtractingThrowable(Level.DEBUG, msg, new Object[]{arg1, arg2}); } public void debug(String msg, Object... args) { doLogExtractingThrowable(Level.DEBUG, msg, args); } public void info(String msg) { log(msg, Level.INFO, (Throwable) null); } public void info(String msg, @Nullable Object arg) { doLogExtractingThrowable(Level.INFO, msg, new Object[]{arg}); } public void info(String msg, @Nullable Object arg1, @Nullable Object arg2) { doLogExtractingThrowable(Level.INFO, msg, new Object[]{arg1, arg2}); } public void info(String msg, Object... args) { doLogExtractingThrowable(Level.INFO, msg, args); } public void warn(String msg) { log(msg, Level.WARN, (Throwable) null); } public void warn(String msg, Throwable thrown) { log(msg, Level.WARN, thrown); } public void warn(String msg, @Nullable Object arg) { doLogExtractingThrowable(Level.WARN, msg, new Object[]{arg}); } public void warn(String msg, @Nullable Object arg1, @Nullable Object arg2) { doLogExtractingThrowable(Level.WARN, msg, new Object[]{arg1, arg2}); } public void warn(String msg, Object... args) { doLogExtractingThrowable(Level.WARN, msg, args); } public void error(String msg) { log(msg, Level.ERROR, (Throwable) null); } public void error(String msg, @Nullable Object arg) { doLogExtractingThrowable(Level.ERROR, msg, new Object[]{arg}); } public void error(String msg, @Nullable Object arg1, @Nullable Object arg2) { doLogExtractingThrowable(Level.ERROR, msg, new Object[]{arg1, arg2}); } public void error(String msg, Object... args) { doLogExtractingThrowable(Level.ERROR, msg, args); } public void error(String msg, Throwable thrown) { log(msg, Level.ERROR, thrown); } private void doLogExtractingThrowable(Level level, String msg, Object[] argArray) { var tuple = MessageFormatter.arrayFormat(msg, argArray); log(tuple.getMessage(), level, tuple.getThrowable()); } private void log(@Nullable String formattedMessage, Level level, @Nullable Throwable t) { if (currentLevel.isMoreVerboseOrEqual(level) && (formattedMessage != null || t != null)) { var stacktrace = t == null ? null : LogOutput.stackTraceToString(t); log(formattedMessage, level, stacktrace); } } private void log(@Nullable String formattedMessage, Level level, @Nullable String stackTrace) { var output = Optional.ofNullable(target.get()).orElseThrow(() -> { var noLogOutputConfigured = new IllegalStateException("No log output configured"); noLogOutputConfigured.printStackTrace(System.err); return noLogOutputConfigured; }); if (output != null) { output.log(formattedMessage, level, stackTrace); Sentry.logger().log(getSentryLogLevel(level), formattedMessage); } } private static SentryLogLevel getSentryLogLevel(Level level) { try { return SentryLogLevel.valueOf(level.name()); } catch (IllegalArgumentException notSupported) { // Current log levels map nicely almost 1:1, but this may change later return SentryLogLevel.ERROR; } } /** * Append an 's' at the end of the word */ public static String singlePlural(int count, String singular) { return count == 1 ? singular : (singular + "s"); } public void setLevel(Level newLevel) { this.currentLevel = newLevel; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/log/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.log; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/Dependency.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.plugins; public record Dependency(SonarArtifact artifact, boolean optional) { public static Dependency optional(SonarArtifact artifact) { return new Dependency(artifact, true); } public static Dependency required(SonarArtifact key) { return new Dependency(key, false); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/EnterpriseReplacement.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.plugins; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; /** * Describes when a plugin is served as its enterprise edition. */ public record EnterpriseReplacement(boolean onSonarQubeCloud, @Nullable Version startingSonarQubeServerVersion) { } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/SonarArtifact.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.plugins; import java.util.Set; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public interface SonarArtifact { /* A key that uniquely identifies the artifact */ String getKey(); /* The list of languages that the artifact supports */ Set getLanguages(); } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/SonarPlugin.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.plugins; import java.util.Arrays; import java.util.EnumSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public enum SonarPlugin implements SonarArtifact { ABAP("abap"), APEX("sonarapex"), C_FAMILY("cpp"), CSHARP_ENTERPRISE("csharpenterprise"), CS_OSS("csharp", CSHARP_ENTERPRISE), COBOL("cobol"), GO("go", new EnterpriseReplacement(true, Version.create("2025.2"))), IAC("iac", new EnterpriseReplacement(true, Version.create("2025.1"))), JAVA("java"), JCL("jcl"), JS("javascript"), KOTLIN("kotlin"), PHP("php"), PLI("pli"), PLSQL("plsql"), PYTHON("python"), RPG("rpg"), RUBY("ruby"), SCALA("sonarscala"), SONARLINT_OMNISHARP("omnisharp", Set.of( Dependency.required(CS_OSS), Dependency.optional(CSHARP_ENTERPRISE), Dependency.required(SonarPluginDependency.OMNISHARP_MONO), Dependency.required(SonarPluginDependency.OMNISHARP_NET472), Dependency.required(SonarPluginDependency.OMNISHARP_NET6))), SWIFT("swift"), TEXT_DEVELOPER("textdeveloper"), TEXT_ENTERPRISE("textenterprise"), TEXT("text", TEXT_DEVELOPER, TEXT_ENTERPRISE), TSQL("tsql"), VBNET_ENTERPRISE("vbnetenterprise"), VBNET_OSS("vbnet", VBNET_ENTERPRISE), WEB("web"), XML("xml"); public static Optional findByKey(String key) { return Arrays.stream(values()).filter(p -> p.key.equals(key)).findFirst(); } /** * Returns {@code true} if the given key is a known enterprise variant with a different * plugin key than its base plugin (e.g. {@code "csharpenterprise"} for CS, * {@code "vbnetenterprise"} for VB.NET). * *

Enterprise editions that share the base plugin key (GO, IAC, TEXT) are not * considered enterprise variants in this sense — they are represented by * {@link #getEnterpriseReplacement()} on the base plugin entry.

*/ public static boolean isEnterpriseVariant(String key) { return Arrays.stream(values()) .flatMap(p -> p.enterpriseVariants.stream()) .anyMatch(ev -> ev.getKey().equals(key)); } /** * Returns the base plugin key for a different-key enterprise variant * (e.g. {@code "csharp"} for {@code "csharpenterprise"}), or empty if the key is not a * known enterprise variant. */ public static Optional basePluginFor(String enterpriseKey) { return Arrays.stream(values()) .filter(p -> p.enterpriseVariants.stream().map(SonarPlugin::getKey).anyMatch(key -> key.equals(enterpriseKey))) .findFirst(); } /** * Returns the base plugin key for a different-key enterprise variant * (e.g. {@code "csharp"} for {@code "csharpenterprise"}), or empty if the key is not a * known enterprise variant. */ public static Optional baseKeyFor(String enterpriseKey) { return basePluginFor(enterpriseKey) .map(SonarPlugin::getKey); } private final String key; /** * Non-empty for plugins that have at least one enterprise variant plugin that uses a * different server key (e.g. {@code CSHARP_ENTERPRISE}). There can be more than one variant. */ private final Set enterpriseVariants; /** * Non-null for plugins whose enterprise edition is a drop-in replacement served under the * same plugin key (GO, IAC, TEXT). */ @Nullable private final EnterpriseReplacement enterpriseReplacement; private final Set dependencies; SonarPlugin(String key) { this.key = key; this.enterpriseVariants = Set.of(); this.enterpriseReplacement = null; this.dependencies = Set.of(); } /** Constructor for plugins with a different-key enterprise variant (CS, VBNET). */ SonarPlugin(String key, SonarPlugin... enterpriseVariants) { this.key = key; this.enterpriseVariants = Set.of(enterpriseVariants); this.enterpriseReplacement = null; this.dependencies = Set.of(); } /** Constructor for same-key enterprise plugins (GO, IAC, TEXT). */ SonarPlugin(String key, EnterpriseReplacement enterpriseReplacement) { this.key = key; this.enterpriseVariants = Set.of(); this.enterpriseReplacement = enterpriseReplacement; this.dependencies = Set.of(); } /** Constructor for plugins with dependencies (e.g. SONARLINT_OMNISHARP). */ SonarPlugin(String key, Set dependencies) { this.key = key; this.enterpriseVariants = Set.of(); this.enterpriseReplacement = null; this.dependencies = dependencies; } @Override public String getKey() { return key; } /** * Returns the enterprise variant plugins (with different keys) for this plugin, if any. Never null. */ public Set getEnterpriseVariants() { return enterpriseVariants; } /** * Returns the enterprise replacement metadata for same-key enterprise plugins (GO, IAC, TEXT), * or empty for all other plugins. */ public Optional getEnterpriseReplacement() { return Optional.ofNullable(enterpriseReplacement); } public Set getDependencies() { return dependencies; } @Override public Set getLanguages() { var sonarLanguages = EnumSet.noneOf(SonarLanguage.class); sonarLanguages.addAll(Arrays.stream(SonarLanguage.values()).filter(l -> l.getPlugin().getKey().equals(key)).collect(Collectors.toSet())); basePluginFor(key).ifPresent(sonarPlugin -> sonarLanguages.addAll(sonarPlugin.getLanguages())); return sonarLanguages; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/SonarPluginDependency.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.plugins; import java.util.Arrays; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public enum SonarPluginDependency implements SonarArtifact { OMNISHARP_MONO("omnisharp-mono"), OMNISHARP_NET472("omnisharp-net472"), OMNISHARP_NET6("omnisharp-net6"); public static Optional findByKey(String key) { return Arrays.stream(values()).filter(p -> p.key.equals(key)).findFirst(); } private final String key; SonarPluginDependency(String key) { this.key = key; } @Override public String getKey() { return key; } /** * All current dependency artifacts are Omnisharp-related and support C# only. * Computed lazily to avoid circular static initialization between SonarLanguage, * SonarPlugin, and SonarPluginDependency. */ @Override public Set getLanguages() { return Set.of(SonarLanguage.CS); } public Set getDependents() { return Arrays.stream(SonarPlugin.values()) .filter(plugin -> plugin.getDependencies().stream().anyMatch(dep -> dep.artifact().equals(this))) .collect(Collectors.toSet()); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/plugins/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.plugins; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/ExecutorServiceShutdownWatchable.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.Deque; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class ExecutorServiceShutdownWatchable implements ExecutorService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final E wrapped; private final Deque> monitorsToCancelOnShutdown = new ConcurrentLinkedDeque<>(); public ExecutorServiceShutdownWatchable(E wrapped) { this.wrapped = wrapped; } public E getWrapped() { return wrapped; } public void cancelOnShutdown(SonarLintCancelMonitor monitor) { if (wrapped.isShutdown()) { monitor.cancel(); } else { monitorsToCancelOnShutdown.add(new WeakReference<>(monitor)); cleanGoneMonitors(); } } private void cleanGoneMonitors() { monitorsToCancelOnShutdown.removeIf(ref -> ref.get() == null); } @Override public void shutdown() { wrapped.shutdown(); cancelMonitors(); } @Override public List shutdownNow() { var result = wrapped.shutdownNow(); cancelMonitors(); return result; } private void cancelMonitors() { monitorsToCancelOnShutdown.forEach(w -> { var monitor = w.get(); if (monitor != null) { try { monitor.cancel(); } catch (Exception e) { LOG.error("Failed to cancel on shutdown", e); } } }); } @Override public boolean isShutdown() { return wrapped.isShutdown(); } @Override public boolean isTerminated() { return wrapped.isTerminated(); } @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { return wrapped.awaitTermination(timeout, unit); } @Override public Future submit(Callable task) { return wrapped.submit(task); } @Override public Future submit(Runnable task, T result) { return wrapped.submit(task, result); } @Override public Future submit(Runnable task) { return wrapped.submit(task); } @Override public List> invokeAll(Collection> tasks) throws InterruptedException { return wrapped.invokeAll(tasks); } @Override public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { return wrapped.invokeAll(tasks, timeout, unit); } @Override public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { return wrapped.invokeAny(tasks); } @Override public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return wrapped.invokeAny(tasks, timeout, unit); } @Override public void execute(Runnable command) { wrapped.execute(command); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/NoOpProgressMonitor.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import javax.annotation.Nullable; public class NoOpProgressMonitor implements ProgressMonitor { @Override public void notifyProgress(@Nullable String message, @Nullable Integer percentage) { // no-op } @Override public boolean isCanceled() { // no-op return false; } @Override public void complete() { // no-op } @Override public void cancel() { // no-op } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/ProgressIndicator.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import javax.annotation.Nullable; public interface ProgressIndicator { void notifyProgress(@Nullable String message, @Nullable Integer percentage); boolean isCanceled(); } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/ProgressMonitor.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; public interface ProgressMonitor extends ProgressIndicator { void complete(); void cancel(); } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/SonarLintCancelMonitor.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import java.util.Deque; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentLinkedDeque; public class SonarLintCancelMonitor { private boolean canceled; private final Deque downstreamCancelAction = new ConcurrentLinkedDeque<>(); public synchronized void cancel() { canceled = true; downstreamCancelAction.forEach(Runnable::run); downstreamCancelAction.clear(); } public boolean isCanceled() { return canceled; } public void checkCanceled() { if (canceled) { throw new CancellationException(); } } public synchronized void onCancel(Runnable action) { if (canceled) { action.run(); } else { this.downstreamCancelAction.add(action); } } public void watchForShutdown(ExecutorServiceShutdownWatchable executorService) { executorService.cancelOnShutdown(this); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/Task.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; public interface Task { void run(ProgressIndicator indicator); } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/TaskManager.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class TaskManager { private static final ProgressMonitor NO_OP = new NoOpProgressMonitor(); private final ConcurrentHashMap progressMonitorsByTaskId = new ConcurrentHashMap<>(); public final void createAndRunTask(@Nullable String configurationScopeId, UUID taskId, String title, @Nullable String message, boolean indeterminate, boolean cancellable, Task task, SonarLintCancelMonitor cancelMonitor) { trackNewTask(taskId, cancelMonitor); runExistingTask(configurationScopeId, taskId, title, message, indeterminate, cancellable, task, cancelMonitor); } public final void runExistingTask(@Nullable String configurationScopeId, UUID taskId, String title, @Nullable String message, boolean indeterminate, boolean cancellable, Task task, SonarLintCancelMonitor cancelMonitor) { var progressMonitor = progressMonitorsByTaskId.get(taskId.toString()); if (progressMonitor == null) { SonarLintLogger.get().debug("Cannot run unknown task '{}'", taskId); return; } startProgress(configurationScopeId, taskId, title, message, indeterminate, cancellable, cancelMonitor); try { task.run(progressMonitor); } finally { progressMonitor.complete(); progressMonitorsByTaskId.remove(taskId.toString()); } } public final void trackNewTask(UUID taskId, SonarLintCancelMonitor cancelMonitor) { var progressMonitor = createProgress(taskId, cancelMonitor); progressMonitorsByTaskId.put(taskId.toString(), progressMonitor); } public void cancel(String taskId) { SonarLintLogger.get().debug("Cancelling task from RPC request {}", taskId); var progressMonitor = progressMonitorsByTaskId.remove(taskId); if (progressMonitor != null) { progressMonitor.cancel(); } } protected void startProgress(@Nullable String configurationScopeId, UUID taskId, String title, @Nullable String message, boolean indeterminate, boolean cancellable, SonarLintCancelMonitor cancelMonitor) { // can be overridden } protected ProgressMonitor createProgress(UUID taskId, SonarLintCancelMonitor cancelMonitor) { // can be overridden return NO_OP; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/progress/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.progress; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/DatabaseExceptionReporter.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import io.sentry.Sentry; import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Central utility for reporting database exceptions to Sentry with relevant context. * Includes simple message-hash deduplication (60 min window) to avoid flooding Sentry. */ public final class DatabaseExceptionReporter { private static final SonarLintLogger LOG = SonarLintLogger.get(); static final String DEDUP_WINDOW_PROPERTY = "sonarlint.internal.db.dedupWindowMs"; private static final long DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000L; // 60 minutes private static final Map recentMessageHashes = new ConcurrentHashMap<>(); private DatabaseExceptionReporter() { } /** * Captures a database exception and reports it to Sentry with contextual tags. * * @param exception the exception to report * @param phase the phase where the exception occurred (e.g., "startup", "runtime", "shutdown") * @param operation the specific operation (e.g., "h2.pool.create", "flyway.migrate", "jooq.execute") * @param sql optional SQL statement that caused the exception */ public static void capture(Throwable exception, String phase, String operation, @Nullable String sql) { var message = exception.getMessage(); if (message != null && isDuplicate(message.hashCode())) { LOG.debug("Skipping duplicate database exception report: {} / {}", phase, operation); return; } LOG.debug("Reporting database exception to Sentry: {} / {}", phase, operation); Sentry.captureException(exception, scope -> { scope.setTag("component", "database"); scope.setTag("db.phase", phase); scope.setTag("db.operation", operation); if (exception instanceof SQLException sqlException) { var sqlState = sqlException.getSQLState(); var errorCode = sqlException.getErrorCode(); if (sqlState != null) { scope.setTag("db.sqlState", sqlState); } scope.setTag("db.errorCode", String.valueOf(errorCode)); } if (sql != null && !sql.isEmpty()) { scope.setExtra("db.sql", truncateSql(sql)); } }); if (message != null) { recordException(message.hashCode()); } } public static void capture(Throwable exception, String phase, String operation) { capture(exception, phase, operation, null); } private static boolean isDuplicate(int messageHash) { var now = System.currentTimeMillis(); cleanupOldEntries(now); var lastReported = recentMessageHashes.get(messageHash); return lastReported != null; } private static void recordException(int messageHash) { recentMessageHashes.put(messageHash, System.currentTimeMillis()); } private static void cleanupOldEntries(long now) { recentMessageHashes.entrySet().removeIf(entry -> (now - entry.getValue()) > getDedupWindowMs()); } private static long getDedupWindowMs() { var property = System.getProperty(DEDUP_WINDOW_PROPERTY); if (property != null) { try { return Long.parseLong(property); } catch (NumberFormatException e) { // ignore, use default } } return DEFAULT_DEDUP_WINDOW_MS; } private static String truncateSql(String sql) { var maxLength = 1000; if (sql.length() <= maxLength) { return sql; } return sql.substring(0, maxLength) + "... [truncated]"; } // Visible for testing static void clearRecentExceptions() { recentMessageHashes.clear(); } // Visible for testing static int getRecentExceptionsCount() { return recentMessageHashes.size(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/JooqDatabaseExceptionListener.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; /** * A jOOQ ExecuteListener that intercepts SQL execution exceptions and reports them * to Sentry via {@link DatabaseExceptionReporter}. */ public class JooqDatabaseExceptionListener implements ExecuteListener { @Override public void exception(ExecuteContext ctx) { var exception = ctx.exception(); if (exception == null) { return; } var sqlException = ctx.sqlException(); var exceptionToReport = sqlException != null ? sqlException : exception; var sql = ctx.sql(); DatabaseExceptionReporter.capture(exceptionToReport, "runtime", "jooq.execute", sql); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/SonarLintDatabase.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.flywaydb.core.Flyway; import org.h2.jdbcx.JdbcConnectionPool; import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.conf.Settings; import org.jooq.impl.DSL; import org.jooq.impl.DefaultConfiguration; import org.jooq.impl.DefaultExecuteListenerProvider; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public final class SonarLintDatabase { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String SQ_IDE_DB_FILENAME = "sq-ide"; private final JdbcConnectionPool dataSource; private final DSLContext dsl; public SonarLintDatabase(Path storageRoot) { JdbcConnectionPool ds; try { var baseDir = storageRoot.resolve("h2"); deleteLegacyDatabase(baseDir); Files.createDirectories(baseDir); var dbBasePath = baseDir.toRealPath().resolve(SQ_IDE_DB_FILENAME).toAbsolutePath(); var url = "jdbc:h2:" + dbBasePath + ";AUTO_SERVER=TRUE"; // Ensure H2 AUTO_SERVER binds and advertises loopback to allow local cross-process connections reliably var bindAddressProperty = "h2.bindAddress"; if (StringUtils.isEmpty(System.getProperty(bindAddressProperty))) { System.setProperty(bindAddressProperty, "127.0.0.1"); } LOG.debug("Initializing H2Database with URL {}", url); ds = JdbcConnectionPool.create(url, "sa", ""); } catch (Exception e) { DatabaseExceptionReporter.capture(e, "startup", "h2.pool.create"); throw new IllegalStateException("Failed to initialize H2Database", e); } this.dataSource = ds; var flyway = Flyway.configure() .dataSource(this.dataSource) .locations("classpath:db/migration") .defaultSchema("PUBLIC") .schemas("PUBLIC") .createSchemas(true) .baselineOnMigrate(true) .failOnMissingLocations(false) .load(); try { flyway.migrate(); } catch (Exception e) { // We are catching the exception for Sentry and rethrowing here to fail starting the backend DatabaseExceptionReporter.capture(e, "startup", "flyway.migrate"); throw e; } System.setProperty("org.jooq.no-tips", "true"); System.setProperty("org.jooq.no-logo", "true"); var jooqConfig = new DefaultConfiguration() .set(this.dataSource) .set(SQLDialect.H2) .set(new Settings().withExecuteLogging(false)) .set(new DefaultExecuteListenerProvider(new JooqDatabaseExceptionListener())); this.dsl = DSL.using(jooqConfig); } private static void deleteLegacyDatabase(Path baseDir) { // see SLCORE-1847 var legacyDb = baseDir.resolve("sonarlint"); if (Files.exists(legacyDb)) { FileUtils.deleteQuietly(legacyDb.toFile()); } } public DSLContext dsl() { return dsl; } public void shutdown() { try { dataSource.dispose(); LOG.debug("H2Database disposed"); } catch (Exception e) { DatabaseExceptionReporter.capture(e, "shutdown", "h2.pool.dispose"); LOG.debug("Error while disposing H2Database: {}", e.getMessage()); } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/XodusPurgeUtils.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class XodusPurgeUtils { private XodusPurgeUtils() { // Static class } private static final SonarLintLogger LOG = SonarLintLogger.get(); public static void deleteInFolderWithPattern(Path folder, String pattern) { if (Files.exists(folder)) { try (var stream = Files.newDirectoryStream(folder, pattern)) { for (var path : stream) { FileUtils.deleteQuietly(path.toFile()); } } catch (Exception e) { LOG.error("Unable to remove files in {} for pattern {}", folder, pattern, e); } } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/adapter/LocalDateAdapter.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.adapter; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.time.LocalDate; public class LocalDateAdapter extends TypeAdapter { @Override public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException { jsonWriter.beginObject() .name("year").value(localDate.getYear()) .name("month").value(localDate.getMonthValue()) .name("day").value(localDate.getDayOfMonth()) .endObject(); } @Override public LocalDate read(JsonReader jsonReader) throws IOException { var year = 0; var month = 0; var day = 0; jsonReader.beginObject(); while(jsonReader.hasNext()) { switch(jsonReader.nextName()) { case "year": year = jsonReader.nextInt(); break; case "month": month = jsonReader.nextInt(); break; case "day": day = jsonReader.nextInt(); break; default: break; } } jsonReader.endObject(); return LocalDate.of(year, month, day); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/adapter/LocalDateTimeAdapter.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.adapter; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; public class LocalDateTimeAdapter extends TypeAdapter { @Override public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException { jsonWriter.beginObject() .name("date"); new LocalDateAdapter().nullSafe().write(jsonWriter, localDateTime.toLocalDate()); jsonWriter.name("time").beginObject() .name("hour").value(localDateTime.getHour()) .name("minute").value(localDateTime.getMinute()) .name("second").value(localDateTime.getSecond()) .name("nano").value(localDateTime.getNano()) .endObject() .endObject(); } @Override public LocalDateTime read(JsonReader jsonReader) throws IOException { LocalDate localDate = null; LocalTime localTime = null; jsonReader.beginObject(); while(jsonReader.hasNext()) { switch(jsonReader.nextName()) { case "date": localDate = new LocalDateAdapter().read(jsonReader); break; case "time": localTime = readTime(jsonReader); break; default: break; } } jsonReader.endObject(); if (localDate == null || localTime == null) { throw new IllegalStateException("Unable to parse LocalDateTime"); } return LocalDateTime.of(localDate, localTime); } private static LocalTime readTime(JsonReader jsonReader) throws IOException { var hour = 0; var minute = 0; var second = 0; var nano = 0; jsonReader.beginObject(); while(jsonReader.hasNext()) { switch(jsonReader.nextName()) { case "hour": hour = jsonReader.nextInt(); break; case "minute": minute = jsonReader.nextInt(); break; case "second": second = jsonReader.nextInt(); break; case "nano": nano = jsonReader.nextInt(); break; default: break; } } jsonReader.endObject(); return LocalTime.of(hour, minute, second, nano); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/adapter/OffsetDateTimeAdapter.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.adapter; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; public class OffsetDateTimeAdapter extends TypeAdapter { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); @Override public void write(JsonWriter jsonWriter, OffsetDateTime offsetDateTime) throws IOException { jsonWriter.value(FORMATTER.format(offsetDateTime)); } @Override public OffsetDateTime read(JsonReader jsonReader) throws IOException { return OffsetDateTime.parse(jsonReader.nextString(), FORMATTER); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/adapter/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.storage.adapter; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/local/FileStorageManager.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.local; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.Base64; import java.util.function.Consumer; import java.util.function.Supplier; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.storage.adapter.LocalDateAdapter; import org.sonarsource.sonarlint.core.commons.storage.adapter.LocalDateTimeAdapter; import org.sonarsource.sonarlint.core.commons.storage.adapter.OffsetDateTimeAdapter; public class FileStorageManager { public static final SonarLintLogger LOG = SonarLintLogger.get(); private final Path path; private final Gson gson; private final Class localStorageType; private final Supplier defaultSupplier; private T inMemoryStorage; private FileTime lastModified; public FileStorageManager(Path path, Supplier defaultSupplier, Class localStorageType) { this.path = path; this.gson = new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter().nullSafe()) .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe()) .create(); this.localStorageType = localStorageType; this.defaultSupplier = defaultSupplier; this.inMemoryStorage = defaultSupplier.get(); } public T getStorage() { if (!path.toFile().exists()) { inMemoryStorage = defaultSupplier.get(); invalidateCache(); } else if (isCacheInvalid()) { refreshInMemoryStorage(); } return inMemoryStorage; } public boolean isCacheInvalid() { try { return lastModified == null || !lastModified.equals(Files.getLastModifiedTime(path)); } catch (IOException e) { LOG.warn("Error checking if cache is invalid", e); return true; } } public void invalidateCache() { lastModified = null; } public synchronized void refreshInMemoryStorage() { try { if (isCacheInvalid()) { inMemoryStorage = read(); updateLastModified(); } } catch (Exception e) { LOG.warn("Error loading data from the file", e); } } public void updateLastModified() throws IOException { lastModified = Files.getLastModifiedTime(path); } public T read() throws IOException { try (var fileChannel = FileChannel.open(path, StandardOpenOption.READ)) { return read(fileChannel); } } public void tryUpdateAtomically(Consumer updater) { try { updateAtomically(updater); } catch (Exception e) { invalidateCache(); LOG.warn("Error updating data in the file", e); } } private synchronized void updateAtomically(Consumer updater) throws IOException { Files.createDirectories(path.getParent()); try (var fileChannel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.SYNC); var ignored = fileChannel.lock()) { var storageData = read(fileChannel); updater.accept(storageData); storageData.validateAndMigrate(); writeAtomically(fileChannel, storageData); inMemoryStorage = storageData; } updateLastModified(); } private T read(FileChannel fileChannel) { try { if (fileChannel.size() == 0) { return defaultSupplier.get(); } final var buf = ByteBuffer.allocate((int) fileChannel.size()); fileChannel.read(buf); var decoded = Base64.getDecoder().decode(buf.array()); var oldJson = new String(decoded, StandardCharsets.UTF_8); var localStorage = gson.fromJson(oldJson, localStorageType); localStorage.validateAndMigrate(); return localStorage; } catch (Exception e) { LOG.warn("Error reading data from file", e); return defaultSupplier.get(); } } private void writeAtomically(FileChannel fileChannel, T newData) throws IOException { fileChannel.truncate(0); var newJson = gson.toJson(newData); var encoded = Base64.getEncoder().encode(newJson.getBytes(StandardCharsets.UTF_8)); fileChannel.write(ByteBuffer.wrap(encoded)); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/local/LocalStorage.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.local; public interface LocalStorage { default void validateAndMigrate() { } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/local/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.storage.local; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/storage/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.storage; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/tracing/Span.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.tracing; import io.sentry.ISpan; import io.sentry.SpanStatus; public class Span { private final ISpan sentrySpan; Span(ISpan sentrySpan) { this.sentrySpan = sentrySpan; } public void finishExceptionally(Throwable throwable) { this.sentrySpan.setThrowable(throwable); this.sentrySpan.finish(SpanStatus.INTERNAL_ERROR); } public void finishSuccessfully() { this.sentrySpan.finish(SpanStatus.OK); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/tracing/Step.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.tracing; import io.sentry.ITransaction; import javax.annotation.Nullable; public class Step { private final String task; private final Runnable operation; public Step(String task, Runnable operation) { this.task = task; this.operation = operation; } public void execute() { operation.run(); } public void executeTransaction(ITransaction transaction, @Nullable String description) { var span = new Span(transaction.startChild(task, description)); try { operation.run(); span.finishSuccessfully(); } catch (Exception exception) { span.finishExceptionally(exception); throw exception; } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/tracing/Trace.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.tracing; import io.sentry.ITransaction; import io.sentry.Sentry; import io.sentry.SpanStatus; import java.util.function.Supplier; import java.util.stream.Stream; import javax.annotation.Nullable; public class Trace { private final ITransaction transaction; private Trace(ITransaction transaction) { this.transaction = transaction; } public static Trace begin(String name, String operation) { return new Trace(Sentry.startTransaction(name, operation)); } public static T startChild(@Nullable Trace trace, String task, @Nullable String description, Supplier operation) { if (trace == null) { return operation.get(); } var span = new Span(trace.transaction.startChild(task, description)); try { var result = operation.get(); span.finishSuccessfully(); return result; } catch (Exception exception) { span.finishExceptionally(exception); throw exception; } } public static void startChild(@Nullable Trace trace, String task, @Nullable String description, Runnable operation) { if (trace == null) { operation.run(); return; } var span = new Span(trace.transaction.startChild(task, description)); try { operation.run(); span.finishSuccessfully(); } catch (Exception exception) { span.finishExceptionally(exception); throw exception; } } public static void startChildren(@Nullable Trace trace, @Nullable String description, Step... steps) { if (trace == null) { Stream.of(steps).forEach(Step::execute); return; } Stream.of(steps).forEach(step -> step.executeTransaction(trace.transaction, description)); } public void setData(String key, Object value) { this.transaction.setData(key, value); } public void setThrowable(Throwable throwable) { this.transaction.setThrowable(throwable); } public void finishExceptionally(Throwable throwable) { this.transaction.setThrowable(throwable); this.transaction.setStatus(SpanStatus.INTERNAL_ERROR); this.transaction.finish(); } public void finishSuccessfully() { this.transaction.setStatus(SpanStatus.OK); this.transaction.finish(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/tracing/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.tracing; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FailSafeExecutors.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * This class should always be preferred to {@link java.util.concurrent.Executors}, except for a few cases regarding RPC read/write threads. */ public class FailSafeExecutors { private static final SonarLintLogger LOG = SonarLintLogger.get(); private FailSafeExecutors() { // utility class } public static ExecutorService newSingleThreadExecutor(String threadName) { return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, threadName)) { @Override protected void afterExecute(Runnable task, @Nullable Throwable throwable) { var extractedThrowable = extractThrowable(task, throwable); if (extractedThrowable != null) { LOG.error("An error occurred while executing a task in " + threadName, extractedThrowable); } super.afterExecute(task, throwable); } }; } public static ScheduledExecutorService newSingleThreadScheduledExecutor(String threadName) { return new ScheduledThreadPoolExecutor(1, r -> new Thread(r, threadName)) { @Override protected void afterExecute(Runnable task, @Nullable Throwable throwable) { var extractedThrowable = extractThrowable(task, throwable); if (extractedThrowable != null) { LOG.error("An error occurred while executing a scheduled task in " + threadName, extractedThrowable); } super.afterExecute(task, throwable); } }; } public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), threadFactory) { @Override protected void afterExecute(Runnable task, @Nullable Throwable throwable) { var extractedThrowable = extractThrowable(task, throwable); if (extractedThrowable != null) { LOG.error("An error occurred while executing a task in " + Thread.currentThread(), extractedThrowable); } super.afterExecute(task, throwable); } }; } @CheckForNull private static Throwable extractThrowable(Runnable task, @Nullable Throwable throwable) { if (throwable != null) { return throwable; } if (task instanceof FutureTask futureTask) { try { if (futureTask.isDone() && !futureTask.isCancelled()) { futureTask.get(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { return e.getCause(); } catch (CancellationException e) { // nothing to do } } return null; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util; import java.net.URI; import java.nio.file.Path; public class FileUtils { public static Path getFilePathFromUri(URI uri) { try { // In case the path contains non-ASCII characters, Path.of() will fail, we should first try to encode the path via toURL() return Path.of(uri.toURL().getPath()); } catch (Exception e) { return Path.of(uri); } } private FileUtils() { // utility class } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/StringUtils.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util; import javax.annotation.Nullable; public class StringUtils { private static final char RTLO = '\u202E'; public static String pluralize(long count, String word) { var pluralizedWord = count == 1 ? word : (word + 's'); return count + " " + pluralizedWord; } public static String sanitizeAgainstRTLO(@Nullable String input) { if (input == null) { return null; } return input.replaceAll(String.valueOf(RTLO), ""); } private StringUtils() { // utility class } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/BlameResult.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.time.Instant; import java.util.List; public record BlameResult(List lineCommitDates) { } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/GitBlameReader.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; public class GitBlameReader { private static final String COMMITTER_MAIL = "committer-mail "; private static final String COMMITTER_TIME = "committer-time "; private static final String NOT_COMMITTED = ""; private final List commitDates = new ArrayList<>(); private boolean isCurrentLineCommitted; public void readLine(String line) { // committer-mail comes before committer-time if (line.startsWith(COMMITTER_MAIL)) { var committerEmail = line.substring(COMMITTER_MAIL.length()); isCurrentLineCommitted = !committerEmail.equals(NOT_COMMITTED); } else if (line.startsWith(COMMITTER_TIME)) { commitDates.add(isCurrentLineCommitted ? Instant.ofEpochSecond(Long.parseLong(line.substring(COMMITTER_TIME.length()))).truncatedTo(ChronoUnit.SECONDS) : null); } } public BlameResult getResult() { return new BlameResult(commitDates); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/GitService.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.file.Path; import java.time.Instant; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.io.FilenameUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.ignore.IgnoreNode; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryBuilder; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.sonar.scm.git.blame.RepositoryBlameCommand; import org.sonarsource.sonarlint.core.commons.MultiFileBlameResult; import org.sonarsource.sonarlint.core.commons.SonarLintGitIgnore; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.git.exceptions.GitException; import org.sonarsource.sonarlint.core.commons.util.git.exceptions.GitRepoNotFoundException; import static java.util.Optional.ofNullable; import static org.eclipse.jgit.lib.Constants.GITIGNORE_FILENAME; public class GitService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final int FILES_GIT_BLAME_TRIGGER_THRESHOLD = 10; private final NativeGitLocator nativeGitLocator; GitService(NativeGitLocator nativeGitLocator) { this.nativeGitLocator = nativeGitLocator; } public static GitService create() { return new GitService(new NativeGitLocator()); } public MultiFileBlameResult getBlameResult(Path projectBaseDir, Set projectBaseRelativeFilePaths, Set fileUris, @Nullable UnaryOperator fileContentProvider, Instant thresholdDate) { var nativeGitExecutable = nativeGitLocator.getNativeGitExecutable(); if (nativeGitExecutable.isEmpty() || fileUris.size() >= FILES_GIT_BLAME_TRIGGER_THRESHOLD) { return blameWithGitFilesBlameLibrary(projectBaseDir, projectBaseRelativeFilePaths, fileContentProvider); } return nativeGitExecutable.get().blame(projectBaseDir, fileUris, thresholdDate); } // Could be optimized to only fetch VCS changed files matching the base dir // Currently, it finds all the files of the git repo, even when called against a subfolder public static Set getVCSChangedFiles(@Nullable Path baseDir) { if (baseDir == null) { return Set.of(); } try (var repo = buildGitRepository(baseDir); var git = new Git(repo)) { var workTreePath = repo.getWorkTree().toPath(); var status = git.status().call(); var uncommitted = status.getUncommittedChanges().stream(); var untracked = status.getUntracked().stream().filter(f -> !f.equals(GITIGNORE_FILENAME)); return Stream.concat(uncommitted, untracked) .map(workTreePath::resolve) .filter(path -> path.normalize().startsWith(baseDir.normalize())) .map(Path::toUri) .collect(Collectors.toSet()); } catch (GitAPIException | GitException e) { LOG.debug("Git repository access error: ", e); return Set.of(); } } /** * Retrieves the Git remote URL for the origin remote from the repository. * * @param baseDir the base directory of the project * @return Optional containing the remote URL if found, empty otherwise */ @CheckForNull public static String getRemoteUrl(@Nullable Path baseDir) { if (baseDir == null) { return null; } try (var gitRepo = buildGitRepository(baseDir)) { var config = gitRepo.getConfig(); return config.getString("remote", "origin", "url"); } catch (GitRepoNotFoundException e) { LOG.debug("Git repository not found for {}", baseDir); return null; } catch (Exception e) { LOG.debug("Error retrieving remote URL for {}: {}", baseDir, e.getMessage()); return null; } } public static MultiFileBlameResult blameWithGitFilesBlameLibrary(Path projectBaseDir, Set projectBaseRelativeFilePaths, @Nullable UnaryOperator fileContentProvider) { LOG.debug("Falling back to JGit"); var startTime = System.currentTimeMillis(); try (var gitRepo = buildGitRepository(projectBaseDir)) { var gitRepoRelativeProjectBaseDir = getRelativePath(gitRepo, projectBaseDir); var gitRepoRelativeFilePaths = projectBaseRelativeFilePaths.stream() .map(gitRepoRelativeProjectBaseDir::resolve) .map(Path::toString) .map(FilenameUtils::separatorsToUnix) .collect(Collectors.toSet()); var blameCommand = new RepositoryBlameCommand(gitRepo) .setTextComparator(RawTextComparator.WS_IGNORE_ALL) .setMultithreading(true) .setFilePaths(gitRepoRelativeFilePaths); ofNullable(fileContentProvider) .ifPresent(provider -> blameCommand.setFileContentProvider(adaptToPlatformBasedPath(provider))); try { var blameResult = blameCommand.call(); var multiFileBlameResult = new MultiFileBlameResult( blameResult.getFileBlameByPath().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new BlameResult(Arrays.asList(e.getValue().getCommitDates())))), gitRepoRelativeProjectBaseDir); LOG.debug("Blamed {} files in {}ms", projectBaseRelativeFilePaths.size(), System.currentTimeMillis() - startTime); return multiFileBlameResult; } catch (NoHeadException e) { // it means that the repository has no commits, so we can't get any blame information return MultiFileBlameResult.empty(gitRepoRelativeProjectBaseDir); } catch (GitAPIException e) { throw new IllegalStateException("Failed to blame repository files", e); } } } private static Path getRelativePath(Repository gitRepo, Path projectBaseDir) { var repoDir = gitRepo.isBare() ? gitRepo.getDirectory() : gitRepo.getWorkTree(); return repoDir.toPath().relativize(projectBaseDir); } private static Repository buildGitRepository(Path basedir) { try { var repositoryBuilder = new RepositoryBuilder() .findGitDir(basedir.toFile()); if (ofNullable(repositoryBuilder.getGitDir()).isEmpty()) { throw new GitRepoNotFoundException(basedir.toString()); } var repository = repositoryBuilder.build(); try (var objReader = repository.getObjectDatabase().newReader()) { // SONARSCGIT-2 Force initialization of shallow commits to avoid later concurrent modification issue objReader.getShallowCommits(); return repository; } } catch (IOException e) { throw new IllegalStateException("Unable to open Git repository", e); } } /** * Assumes the supplied {@param baseDir} or some of its parents is a git repository. * If error occurs during parsing .gitignore file then an ignore node with no rules is created -> Files checked against this node will be considered as not ignored. */ public static SonarLintGitIgnore createSonarLintGitIgnore(@Nullable Path baseDir) { if (baseDir == null) { return new SonarLintGitIgnore(new IgnoreNode()); } try (var gitRepo = buildGitRepository(baseDir)) { var ignoreNode = buildIgnoreNode(gitRepo); return new SonarLintGitIgnore(ignoreNode); } catch (GitRepoNotFoundException e) { LOG.info("Git Repository not found for {}. The path {} is not in a Git repository", baseDir, e.getPath()); } catch (FileNotFoundException e) { LOG.info(".gitignore file was not found for {}", baseDir); } catch (Exception e) { LOG.warn("Error occurred while reading .gitignore file: ", e); LOG.warn("Building empty ignore node with no rules. Files checked against this node will be considered as not ignored."); } return new SonarLintGitIgnore(new IgnoreNode()); } private static IgnoreNode buildIgnoreNode(Repository repository) throws IOException { var ignoreNode = new IgnoreNode(); if (repository.isBare()) { readGitIgnoreFileFromBareRepo(repository, ignoreNode); } else { readIgnoreFileFromNonBareRepo(repository, ignoreNode); } return ignoreNode; } private static void readGitIgnoreFileFromBareRepo(Repository repository, IgnoreNode ignoreNode) throws IOException { var loader = readFileContentFromGitRepo(repository, GITIGNORE_FILENAME); if (loader.isPresent()) { try (InputStream inputStream = loader.get().openStream()) { ignoreNode.parse(inputStream); } } } private static void readIgnoreFileFromNonBareRepo(Repository repository, IgnoreNode ignoreNode) throws IOException { var rootDir = repository.getWorkTree(); var gitIgnoreFile = new File(rootDir, GITIGNORE_FILENAME); try (var inputStream = new FileInputStream(gitIgnoreFile)) { ignoreNode.parse(inputStream); } } private static UnaryOperator adaptToPlatformBasedPath(UnaryOperator provider) { return unixPath -> { var platformBasedPath = Path.of(unixPath).toString(); return provider.apply(platformBasedPath); }; } private static Optional readFileContentFromGitRepo(Repository repository, String fileName) throws IOException { var headId = repository.resolve(Constants.HEAD); if (headId == null) { // No commits in the repository return Optional.empty(); } try (var revWalk = new RevWalk(repository)) { var commit = revWalk.parseCommit(headId); try (var treeWalk = new TreeWalk(repository)) { treeWalk.addTree(commit.getTree()); treeWalk.setRecursive(true); treeWalk.setFilter(org.eclipse.jgit.treewalk.filter.PathFilter.create(fileName)); if (!treeWalk.next()) { return Optional.empty(); } return Optional.of(repository.open(treeWalk.getObjectId(0))); } } } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGit.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.net.URI; import java.nio.file.Path; import java.time.Instant; import java.time.Period; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.MultiFileBlameResult; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FileUtils; import static java.lang.String.format; public class NativeGit { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Version MINIMUM_REQUIRED_GIT_VERSION = Version.create("2.24"); private static final String GIT_VERSION_OUTPUT_PREFIX = "git version"; private static final Period BLAME_HISTORY_WINDOW = Period.ofDays(365); private final String executable; public NativeGit(String executable) { this.executable = executable; } public boolean isSupportedVersion() { return version() .filter(version -> version.satisfiesMinRequirement(MINIMUM_REQUIRED_GIT_VERSION)) .isPresent(); } private Optional version() { var lines = new ArrayList(); var success = executeGitCommand(null, lines::add, executable, "--version"); return success ? parseGitVersionOutput(lines) : Optional.empty(); } static Optional parseGitVersionOutput(List lines) { var version = lines.stream().findFirst() .map(String::trim) .filter(line -> line.startsWith(GIT_VERSION_OUTPUT_PREFIX)) .map(line -> line.substring(GIT_VERSION_OUTPUT_PREFIX.length())) .map(String::trim) .map(actualVersion -> actualVersion.split("\\.", 3)) .filter(versionParts -> versionParts.length > 1) .flatMap(NativeGit::tryCreateVersion); if (version.isEmpty()) { LOG.debug("Cannot parse git --version output: {}", String.join("\n", lines)); } return version; } private static Optional tryCreateVersion(String[] versionParts) { try { // keep only MAJOR and MINOR numbers, it's sufficient for checking support return Optional.of(Version.create(versionParts[0] + "." + versionParts[1])); } catch (Exception e) { // error will be logged above } return Optional.empty(); } public MultiFileBlameResult blame(Path projectBaseDir, Set fileUris, Instant thresholdDateFromNewCodeDefinition) { LOG.debug("Using native git blame"); var startTime = System.currentTimeMillis(); var blamePerFile = new HashMap(); for (var fileUri : fileUris) { var filePath = FileUtils.getFilePathFromUri(fileUri).toAbsolutePath().toString(); var filePathUnix = filePath.replace("\\", "/"); var yearAgo = Instant.now().minus(BLAME_HISTORY_WINDOW); var thresholdDate = thresholdDateFromNewCodeDefinition.isAfter(yearAgo) ? yearAgo : thresholdDateFromNewCodeDefinition; var blameHistoryThresholdCondition = "--since='" + thresholdDate + "'"; var command = new String[] {executable, "blame", blameHistoryThresholdCondition, filePath, "--line-porcelain", "--encoding=UTF-8"}; var blameReader = new GitBlameReader(); var success = executeGitCommand(projectBaseDir, blameReader::readLine, command); if (success) { blamePerFile.put(filePathUnix, blameReader.getResult()); } } LOG.debug("Blamed {} files in {}ms", fileUris.size(), System.currentTimeMillis() - startTime); return new MultiFileBlameResult(blamePerFile, projectBaseDir); } private static boolean executeGitCommand(@Nullable Path workingDir, Consumer lineConsumer, String... command) { var output = new ProcessWrapperFactory() .create(workingDir, lineConsumer, command) .execute(); if (output.exitCode() == 0) { return true; } LOG.debug(format("Command failed with code: %d", output.exitCode())); return false; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitLocator.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; import java.util.function.Consumer; import org.apache.commons.lang3.SystemUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class NativeGitLocator { private static final SonarLintLogger LOG = SonarLintLogger.get(); // So we only have to make the expensive call once (or at most twice) to get the native Git executable private boolean checkedForNativeGitExecutable = false; private NativeGit nativeGitExecutable = null; /** * Get the native Git executable by checking for the version of both `git` and `git.exe`. We cache this information * to not run these expensive processes more than once (or twice in case of Windows). */ public Optional getNativeGitExecutable() { if (checkedForNativeGitExecutable) { return Optional.ofNullable(nativeGitExecutable); } var nativeGit = getGitExecutable() .map(NativeGit::new) .filter(NativeGit::isSupportedVersion); checkedForNativeGitExecutable = true; nativeGitExecutable = nativeGit.orElse(null); return nativeGit; } Optional getGitExecutable() { return SystemUtils.IS_OS_WINDOWS ? locateGitOnWindows() : Optional.of("git"); } private static Optional locateGitOnWindows() { var lines = new ArrayList(); var result = callWhereTool(lines::add); return locateGitOnWindows(result, String.join("\n", lines)); } static Optional locateGitOnWindows(ProcessWrapperFactory.ProcessExecutionResult result, String lines) { // Windows will search current directory in addition to the PATH variable, which is unsecure. // To avoid it we use where.exe to find git binary only in PATH. if (result.exitCode() == 0 && lines.contains("git.exe")) { var out = Arrays.stream(lines.split(System.lineSeparator())).map(String::trim).findFirst(); LOG.debug("Found git.exe at {}", out); return out; } LOG.debug("git.exe not found in PATH. PATH value was: " + System.getProperty("PATH")); return Optional.empty(); } private static ProcessWrapperFactory.ProcessExecutionResult callWhereTool(Consumer lineConsumer) { LOG.debug("Looking for git command in the PATH using where.exe (Windows)"); return new ProcessWrapperFactory() .create(null, lineConsumer, "C:\\Windows\\System32\\where.exe", "$PATH:git.exe") .execute(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/ProcessWrapperFactory.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Path; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static java.lang.String.format; import static java.lang.String.join; import static java.nio.charset.StandardCharsets.UTF_8; public class ProcessWrapperFactory { private static final SonarLintLogger LOG = SonarLintLogger.get(); public ProcessWrapperFactory() { // nothing to do } public ProcessWrapper create(@Nullable Path baseDir, Consumer lineConsumer, String... command) { return new ProcessWrapper(baseDir, lineConsumer, command); } public static class ProcessWrapper { private final Path baseDir; private final Consumer lineConsumer; private final String[] command; ProcessWrapper(@Nullable Path baseDir, Consumer lineConsumer, String... command) { this.baseDir = baseDir; this.lineConsumer = lineConsumer; this.command = command; } void processInputStream(InputStream inputStream, Consumer stringConsumer) throws IOException { try (var reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) { String line; while ((line = reader.readLine()) != null) { stringConsumer.accept(line); } } } public ProcessExecutionResult execute() { Process p; try { p = createProcess(); } catch (IOException e) { LOG.warn(format("Could not execute command: [%s]", join(" ", command)), e); return new ProcessExecutionResult(-2); } try { return runProcessAndGetOutput(p); } catch (InterruptedException e) { LOG.warn(format("Command [%s] interrupted", join(" ", command)), e); Thread.currentThread().interrupt(); } catch (Exception e) { LOG.warn(format("Command failed: [%s]", join(" ", command)), e); } finally { p.destroy(); } return new ProcessExecutionResult(-1); } Process createProcess() throws IOException { return new ProcessBuilder() .command(command) .directory(baseDir != null ? baseDir.toFile() : null) .start(); } ProcessExecutionResult runProcessAndGetOutput(Process p) throws InterruptedException, IOException { processInputStream(p.getInputStream(), lineConsumer); processInputStream(p.getErrorStream(), line -> { if (!line.isBlank()) { LOG.debug(line); } }); int exit = p.waitFor(); return new ProcessExecutionResult(exit); } } public record ProcessExecutionResult(int exitCode) { } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/exceptions/GitException.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git.exceptions; public class GitException extends RuntimeException { private final String path; public GitException(String path) { this.path = path; } public String getPath() { return path; } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/exceptions/GitRepoNotFoundException.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git.exceptions; public class GitRepoNotFoundException extends GitException { public GitRepoNotFoundException(String path) { super(path); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/exceptions/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.util.git.exceptions; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.util.git; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.util; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/validation/InvalidFields.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.validation; import java.util.ArrayList; import java.util.List; public class InvalidFields { private final List names = new ArrayList<>(); public void add(String name) { names.add(name); } public String[] getNames() { return names.toArray(new String[0]); } public boolean hasInvalidFields() { return !names.isEmpty(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/validation/RegexpValidator.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.validation; import java.util.Map; import java.util.regex.Pattern; public class RegexpValidator { private final Pattern pattern; public RegexpValidator(String regexp) { this.pattern = Pattern.compile(regexp); } public InvalidFields validateAll(Map namedValues) { var invalidFields = new InvalidFields(); namedValues.entrySet().stream() .filter(this::isInvalid) .map(Map.Entry::getKey) .forEach(invalidFields::add); return invalidFields; } private boolean isInvalid(Map.Entry nameValue) { return !pattern.matcher(nameValue.getValue()).matches(); } } ================================================ FILE: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/validation/package-info.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons.validation; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/commons/src/main/resources/db/migration/README_BEFORE_TOUCHING_THIS_FOLDER.md ================================================ Flyway computes checksums of migration files and saves them in the DB. Once a migration has been shipped and applied by some users, it is forbidden to modify a migration file, as the checksum would differ. This would fail the migration validation at startup, preventing SQ-IDE from starting. As a rule of thumb, please do not modify a migration file after it has been merged to master, because we cannot know if it has been applied by users. Instead, please add a new migration file, by respecting the pattern VX__description.sql, and the numbering (+1 compared to the latest migration). ================================================ FILE: backend/commons/src/main/resources/db/migration/V1__init_database.sql ================================================ CREATE TABLE IF NOT EXISTS AI_CODEFIX_SETTINGS ( connection_id VARCHAR(255) NOT NULL PRIMARY KEY, supported_rules VARCHAR(200) ARRAY, organization_eligible BOOLEAN, enablement VARCHAR(64), enabled_project_keys VARCHAR(400) ARRAY, CONSTRAINT pk_ai_codefix_settings PRIMARY KEY (connection_id) ); CREATE TABLE IF NOT EXISTS KNOWN_FINDINGS ( -- UUID id UUID NOT NULL PRIMARY KEY, configuration_scope_id VARCHAR(255) NOT NULL, ide_relative_file_path VARCHAR(255) NOT NULL, server_key VARCHAR(255), rule_key VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL, introduction_date TIMESTAMP NOT NULL, finding_type VARCHAR(255) NOT NULL, -- TextRangeWithHash start_line INT, start_line_offset INT, end_line INT, end_line_offset INT, text_range_hash VARCHAR(255), -- LineWithHash line INT, line_hash VARCHAR(255) ); CREATE TABLE IF NOT EXISTS SERVER_FINDINGS ( id UUID, connection_id VARCHAR(255) NOT NULL, sonar_project_key VARCHAR(255) NOT NULL, server_key VARCHAR(255) NOT NULL PRIMARY KEY, rule_id VARCHAR(255), rule_key VARCHAR(255) NOT NULL, message VARCHAR(10000) NOT NULL, file_path VARCHAR(4096) NOT NULL, -- default Linux path length limit creation_date TIMESTAMP NOT NULL, user_severity VARCHAR(255), rule_type VARCHAR(255), rule_description_context_key VARCHAR(255), clean_code_attribute VARCHAR(255), finding_type VARCHAR(255) NOT NULL, branch_name VARCHAR(255), vulnerability_probability VARCHAR(255), assignee VARCHAR(255), impacts JSON(1000), flows JSON(10000000), -- Resolution resolved BOOLEAN, issue_resolution_status VARCHAR(255), hotspot_review_status VARCHAR(255), -- TextRangeWithHash start_line INT, start_line_offset INT, end_line INT, end_line_offset INT, text_range_hash VARCHAR(255), -- LineWithHash line INT, line_hash VARCHAR(255) ); CREATE TABLE IF NOT EXISTS SERVER_BRANCHES ( branch_name VARCHAR(255) NOT NULL, connection_id VARCHAR(255) NOT NULL, sonar_project_key VARCHAR(255) NOT NULL, last_issue_sync_ts TIMESTAMP, last_taint_sync_ts TIMESTAMP, last_hotspot_sync_ts TIMESTAMP, last_issue_enabled_langs VARCHAR(64) ARRAY, last_taint_enabled_langs VARCHAR(64) ARRAY, last_hotspot_enabled_langs VARCHAR(64) ARRAY, PRIMARY KEY (branch_name, connection_id, sonar_project_key) ); CREATE TABLE IF NOT EXISTS SERVER_DEPENDENCY_RISKS ( id UUID NOT NULL PRIMARY KEY, connection_id VARCHAR(255) NOT NULL, sonar_project_key VARCHAR(255) NOT NULL, branch_name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, severity VARCHAR(255) NOT NULL, software_quality VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, package_name VARCHAR(255) NOT NULL, package_version VARCHAR(255) NOT NULL, vulnerability_id VARCHAR(255), cvss_score VARCHAR(255), transitions JSON(10000) NOT NULL ); ================================================ FILE: backend/commons/src/main/resources/db/migration/V2__create_local_only_issues_table.sql ================================================ -- Flyway migration: create LOCAL_ONLY_ISSUES table for H2 -- This table stores local-only issues (issues detected locally but not yet on the server) CREATE TABLE IF NOT EXISTS LOCAL_ONLY_ISSUES ( id UUID NOT NULL PRIMARY KEY, configuration_scope_id VARCHAR(255) NOT NULL, server_relative_path VARCHAR(1000) NOT NULL, rule_key VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL, -- Resolution fields (nullable when issue is not resolved) resolution_status VARCHAR(50), resolution_date TIMESTAMP, comment VARCHAR(255), -- TextRangeWithHash fields (nullable) start_line INT, start_line_offset INT, end_line INT, end_line_offset INT, text_range_hash VARCHAR(255), -- LineWithHash fields (nullable) line INT, line_hash VARCHAR(255) ); CREATE INDEX IF NOT EXISTS idx_local_only_issues_config_scope_file ON LOCAL_ONLY_ISSUES(configuration_scope_id, server_relative_path); CREATE INDEX IF NOT EXISTS idx_local_only_issues_resolution_date ON LOCAL_ONLY_ISSUES(resolution_date); ================================================ FILE: backend/commons/src/main/resources/db/migration/V3__allow_longer_messages_for_known_findings_table.sql ================================================ ALTER TABLE KNOWN_FINDINGS ALTER COLUMN message SET DATA TYPE VARCHAR(10000); ALTER TABLE LOCAL_ONLY_ISSUES ALTER COLUMN message SET DATA TYPE VARCHAR(10000); ================================================ FILE: backend/commons/src/main/resources/db/migration/V4__allow_longer_file_paths.sql ================================================ ALTER TABLE KNOWN_FINDINGS ALTER COLUMN ide_relative_file_path SET DATA TYPE VARCHAR; ALTER TABLE SERVER_FINDINGS ALTER COLUMN file_path SET DATA TYPE VARCHAR; ALTER TABLE LOCAL_ONLY_ISSUES ALTER COLUMN server_relative_path SET DATA TYPE VARCHAR; ================================================ FILE: backend/commons/src/main/resources/db/migration/V5__allow_longer_configuration_scope_ids.sql ================================================ ALTER TABLE KNOWN_FINDINGS ALTER COLUMN configuration_scope_id SET DATA TYPE VARCHAR; ALTER TABLE LOCAL_ONLY_ISSUES ALTER COLUMN configuration_scope_id SET DATA TYPE VARCHAR; ================================================ FILE: backend/commons/src/main/resources/logback-shared.xml ================================================ ================================================ FILE: backend/commons/src/main/resources/sl_core_version.txt ================================================ ${project.version} ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/HotspotReviewStatusTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class HotspotReviewStatusTest { @Test void should_be_resolved_when_fixed_or_safe() { assertThat(HotspotReviewStatus.SAFE.isResolved()).isTrue(); assertThat(HotspotReviewStatus.FIXED.isResolved()).isTrue(); assertThat(HotspotReviewStatus.ACKNOWLEDGED.isResolved()).isFalse(); assertThat(HotspotReviewStatus.TO_REVIEW.isResolved()).isFalse(); } @Test void should_be_reviewed_when_fixed_or_safe_or_acknowledged() { assertThat(HotspotReviewStatus.SAFE.isReviewed()).isTrue(); assertThat(HotspotReviewStatus.FIXED.isReviewed()).isTrue(); assertThat(HotspotReviewStatus.ACKNOWLEDGED.isReviewed()).isTrue(); assertThat(HotspotReviewStatus.TO_REVIEW.isReviewed()).isFalse(); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/IOExceptionUtilsTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.io.IOException; import java.util.ArrayDeque; import java.util.List; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.throwFirstWithOtherSuppressed; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.tryAndCollectIOException; class IOExceptionUtilsTests { @Test void test_tryAndCollectIOException_no_exceptions() { var list = new ArrayDeque(); tryAndCollectIOException(() -> { }, list); assertThat(list).isEmpty(); } @Test void test_tryAndCollectIOException_one_exception() { var list = new ArrayDeque(); tryAndCollectIOException(() -> { throw new IOException("e1"); }, list); assertThat(list).hasSize(1); } @Test void test_tryAndCollectIOException_multiple_exceptions() { var list = new ArrayDeque(); tryAndCollectIOException(() -> { throw new IOException("e1"); }, list); tryAndCollectIOException(() -> { throw new IOException("e2"); }, list); assertThat(list).extracting(IOException::getMessage).containsExactlyInAnyOrder("e1", "e2"); } @Test void test_throwFirstWithOtherSuppressed_no_exceptions() { assertDoesNotThrow(() -> throwFirstWithOtherSuppressed(new ArrayDeque<>(List.of()))); } @Test void test_throwFirstWithOtherSuppressed_one_exception() { var thrown = assertThrows(IOException.class, () -> throwFirstWithOtherSuppressed(new ArrayDeque<>(List.of(new IOException("e1"))))); assertThat(thrown).hasMessage("e1").hasNoSuppressedExceptions(); } @Test void test_throwFirstWithOtherSuppressed_multiple_exceptions() { var thrown = assertThrows(IOException.class, () -> throwFirstWithOtherSuppressed(new ArrayDeque<>(List.of(new IOException("e1"), new IOException("e2"), new IOException("e3"))))); assertThat(thrown).hasMessage("e1").hasSuppressedException(new IOException("e2")).hasSuppressedException(new IOException("e3")); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/LogTestStartAndEnd.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; public class LogTestStartAndEnd implements BeforeEachCallback, AfterEachCallback { @Override public void beforeEach(ExtensionContext extensionContext) { extensionContext.getTestMethod().ifPresent(method -> System.out.printf(">>> Before test %s%n", method.getName())); } @Override public void afterEach(ExtensionContext extensionContext) { extensionContext.getTestMethod().ifPresent(method -> System.out.printf("<<< After test %s%n", method.getName())); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/MultiFileBlameResultTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.util.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.appendFile; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.commit; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.commitAtDate; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createFile; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createRepository; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.modifyFile; import static org.sonarsource.sonarlint.core.commons.util.git.GitService.blameWithGitFilesBlameLibrary; class MultiFileBlameResultTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private Git git; @TempDir private Path projectDir; @BeforeEach void prepare() throws IOException, GitAPIException { git = createRepository(projectDir); } @AfterEach void cleanup() throws IOException { FileUtils.delete(projectDir.toFile(), RECURSIVE); } @Test void it_should_return_correct_latest_changed_date_for_file_lines() throws IOException, GitAPIException, InterruptedException { createFile(projectDir, "fileA", "line1", "line2", "line3"); var c1 = commit(git, "fileA"); // Wait for one second to achieve different commit time TimeUnit.MILLISECONDS.sleep(10); appendFile(projectDir.resolve("fileA"), "new line 4"); var c2 = commit(git, "fileA"); // Wait for one second to achieve different commit time TimeUnit.MILLISECONDS.sleep(10); createFile(projectDir, "fileB", "line1", "line2", "line3"); var c3 = commit(git, "fileB"); createFile(projectDir, "fileC", "line1", "line2", "line3"); commit(git, "fileC"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA"), Path.of("fileB")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2))).isPresent().contains(c1); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(2, 3))).isPresent().contains(c1); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(3, 4))).isPresent().contains(c2); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileB"), List.of(1, 2))).isPresent().contains(c3); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileC"), List.of(1, 2))).isEmpty(); } @Test void it_should_handle_all_line_modified() throws IOException, GitAPIException { createFile(projectDir, "fileA", "line1", "line2", "line3"); var c1 = commit(git, "fileA"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2, 3))).isPresent().contains(c1); modifyFile(projectDir.resolve("fileA"), "new line1", "new line2", "new line3"); results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2, 3))).isEmpty(); } @Test void it_should_return_latest_change_date() throws IOException, GitAPIException { createFile(projectDir, "fileA", "line1", "line2", "line3"); var now = Instant.now(); var c1 = commitAtDate(git, now.minus(1, ChronoUnit.DAYS), "fileA"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2, 3))).isPresent().contains(c1); modifyFile(projectDir.resolve("fileA"), "line1", "line2", "new line3"); commitAtDate(git, now, "fileA"); results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); var result = results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2, 3)); assertThat(result).isPresent(); assertThat(ChronoUnit.MINUTES.between(result.get(), now)).isZero(); } @Test void it_should_handle_end_of_line_modified() throws IOException, GitAPIException { createFile(projectDir, "fileA", "line1", "line2"); var c1 = commit(git, "fileA"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2))).isPresent().contains(c1); appendFile(projectDir.resolve("fileA"), "new line3", "new line4"); results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2, 3))).isEmpty(); } @Test void it_should_handle_dodgy_input() throws IOException, GitAPIException { createFile(projectDir, "fileA", "line1", "line2", "line3"); var c1 = commit(git, "fileA"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of("fileA"), Path.of("fileB")), null); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), IntStream.rangeClosed(1, 100).boxed().toList())).isPresent().contains(c1); assertThat(results.getLatestChangeDateForLinesInFile(Path.of("fileA"), IntStream.rangeClosed(100, 1000).boxed().toList())).isEmpty(); } @Test void it_should_raise_exception_if_wrong_line_numbering_provided() throws IOException, GitAPIException { createFile(projectDir, "fileA", "line1", "line2", "line3"); commit(git, "fileA"); var fileA = Path.of("fileA"); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(fileA), null); var invalidLineNumbers = List.of(0, 1, 2); assertThrows(IllegalArgumentException.class, () -> results.getLatestChangeDateForLinesInFile(fileA, invalidLineNumbers)); } @Test void it_should_handle_files_within_inner_dir() throws IOException, GitAPIException { var deepFilePath = Path.of("innerDir").resolve("fileA").toString(); createFile(projectDir, deepFilePath, "line1", "line2", "line3"); var c1 = commit(git, deepFilePath); var results = blameWithGitFilesBlameLibrary(projectDir, Set.of(Path.of(deepFilePath)), null); assertThat(results.getLatestChangeDateForLinesInFile( Path.of(deepFilePath), IntStream.rangeClosed(1, 100).boxed().toList())) .isPresent().contains(c1); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/NewCodeDefinitionTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.time.Instant; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class NewCodeDefinitionTests { @Test void isOnNewCodeTest() { var analysisDate = Instant.parse("2023-09-12T10:15:30.00Z"); var issueCreationDateBeforeAnalysis = Instant.parse("2023-09-10T10:15:30.00Z"); var issueCreationDateAfterAnalysis = Instant.parse("2023-09-14T10:15:30.00Z"); var newCodeDefinitionWithoutDate = NewCodeDefinition.withNumberOfDaysWithDate(30, 0); var newCodeDefinitionWithDate = NewCodeDefinition.withNumberOfDaysWithDate(30, analysisDate.toEpochMilli()); assertThat(newCodeDefinitionWithoutDate.isOnNewCode(analysisDate.toEpochMilli())).isTrue(); assertThat(newCodeDefinitionWithDate.isOnNewCode(issueCreationDateAfterAnalysis.toEpochMilli())).isTrue(); assertThat(newCodeDefinitionWithDate.isOnNewCode(issueCreationDateBeforeAnalysis.toEpochMilli())).isFalse(); } @Test void toStringTest() { var analysisEpochDate = Instant.parse("2023-09-12T10:15:30.00Z").toEpochMilli(); var numberOfDays = NewCodeDefinition.withNumberOfDaysWithDate(30, analysisEpochDate); var previousVersionNull = NewCodeDefinition.withPreviousVersion(analysisEpochDate, null); var previousVersion = NewCodeDefinition.withPreviousVersion(analysisEpochDate, "version"); var specificAnalysis = NewCodeDefinition.withSpecificAnalysis(analysisEpochDate); var referenceBranch = NewCodeDefinition.withReferenceBranch("referenceBranch"); var analysisDate = NewCodeDefinition.formatEpochToDate(analysisEpochDate); assertThat(numberOfDays).hasToString("From last 30 days"); assertThat(previousVersionNull).hasToString("Since " + analysisDate); assertThat(previousVersion).hasToString("Since version version"); assertThat(specificAnalysis).hasToString("Since analysis from " + analysisDate); assertThat(referenceBranch).hasToString("Current new code definition (reference branch) is not supported"); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/RuleKeyTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class RuleKeyTests { @Test void test_ruleKey_accessors() { var repository = "squid"; var rule = "1181"; var ruleKey = new RuleKey(repository, rule); assertThat(ruleKey.repository()).isEqualTo(repository); assertThat(ruleKey.rule()).isEqualTo(rule); assertThat(ruleKey).hasToString(repository + ":" + rule); } @Test void ruleKey_equals_and_hashcode() { var repository = "squid"; var rule = "1181"; var ruleKey1 = new RuleKey(repository, rule); var ruleKey2 = new RuleKey(repository, rule); assertThat(ruleKey1) .isEqualTo(ruleKey1) .isEqualTo(ruleKey2) .hasSameHashCodeAs(ruleKey2) .isNotEqualTo(null) .isNotEqualTo(new RuleKey(repository, rule + "x")) .isNotEqualTo(new RuleKey(repository + "x", rule)); } @Test void ruleKey_equals_to_its_parsed_from_toString() { var repository = "squid"; var rule = "1181"; var ruleKey1 = new RuleKey(repository, rule); var ruleKey2 = RuleKey.parse(ruleKey1.toString()); assertThat(ruleKey2).isEqualTo(ruleKey1); } @Test void parse_throws_for_illegal_format() { assertThrows(IllegalArgumentException.class, () -> { RuleKey.parse("foo"); }); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/SonarLintCoreVersionTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class SonarLintCoreVersionTests { @Test void testVersionFallback() { var version = SonarLintCoreVersion.getLibraryVersion(); assertThat(isVersion(version)).isTrue(); } @Test void testVersion() { var version = SonarLintCoreVersion.get(); assertThat(isVersion(version)).isTrue(); } @Test void testVersionAssert() { assertThat(isVersion("2.1")).isTrue(); assertThat(isVersion("2.0-SNAPSHOT")).isTrue(); assertThat(isVersion("2.0.0-SNAPSHOT")).isTrue(); assertThat(isVersion("2-SNAPSHOT")).isFalse(); assertThat(isVersion("unknown")).isFalse(); assertThat(isVersion(null)).isFalse(); } private boolean isVersion(String version) { if (version == null) { return false; } var regex = "(\\d+\\.\\d+(?:\\.\\d+)*).*"; var pattern = Pattern.compile(regex); var matcher = pattern.matcher(version); return matcher.find(); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/SonarLintUserHomeTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class SonarLintUserHomeTests { @Test void env_setting_should_override_default_home() { var customHome = "/custom/home"; assertThat(SonarLintUserHome.home(customHome)).isEqualTo(Paths.get(customHome)); } @Test void default_home_should_be_in_user_home() { assertThat(SonarLintUserHome.get()).isEqualTo(Paths.get(System.getProperty("user.home")).resolve(".sonarlint")); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/StringUtilsTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.commons.util.StringUtils.pluralize; import static org.sonarsource.sonarlint.core.commons.util.StringUtils.sanitizeAgainstRTLO; class StringUtilsTests { @Test void should_pluralize_words() { assertThat(pluralize(0, "word")).isEqualTo("0 words"); assertThat(pluralize(1, "word")).isEqualTo("1 word"); assertThat(pluralize(2, "word")).isEqualTo("2 words"); } @Test void should_sanitize_against_rtlo() { assertThat(sanitizeAgainstRTLO("This is a \u202eegassem")).isEqualTo("This is a egassem"); } @Test void should_sanitize_with_null() { assertThat(sanitizeAgainstRTLO(null)).isNull(); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/TextRangeTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.api.TextRange; import static org.assertj.core.api.Assertions.assertThat; class TextRangeTests { @Test void test_getters() { var textRange = new TextRange(1, 2, 3, 4); assertThat(textRange.getStartLine()).isEqualTo(1); assertThat(textRange.getStartLineOffset()).isEqualTo(2); assertThat(textRange.getEndLine()).isEqualTo(3); assertThat(textRange.getEndLineOffset()).isEqualTo(4); } @Test void test_equals_hashcode() { var textRange = new TextRange(1, 2, 3, 4); assertThat(textRange).hasSameHashCodeAs(new TextRange(1, 2, 3, 4)) .isEqualTo(textRange) .isEqualTo(new TextRange(1, 2, 3, 4)) .isNotEqualTo(new TextRange(11, 2, 3, 4)) .isNotEqualTo(new TextRange(1, 22, 3, 4)) .isNotEqualTo(new TextRange(1, 2, 33, 4)) .isNotEqualTo(new TextRange(1, 2, 3, 44)) .isNotEqualTo("foo"); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/TextRangeWithHashTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import static org.assertj.core.api.Assertions.assertThat; class TextRangeWithHashTests { @Test void test_getters() { var textRange = new TextRangeWithHash(1, 2, 3, 4, "md5"); assertThat(textRange.getHash()).isEqualTo("md5"); } @Test void test_equals_hashcode() { var textRange = new TextRangeWithHash(1, 2, 3, 4, "md5"); assertThat(textRange).hasSameHashCodeAs(new TextRangeWithHash(1, 2, 3, 4, "md5")) .isEqualTo(textRange) .isEqualTo(new TextRangeWithHash(1, 2, 3, 4, "md5")) .isNotEqualTo(new TextRange(1, 2, 3, 4)) .isNotEqualTo(new TextRangeWithHash(11, 2, 3, 4, "md5")) .isNotEqualTo(new TextRangeWithHash(1, 22, 3, 4, "md5")) .isNotEqualTo(new TextRangeWithHash(1, 2, 33, 4, "md5")) .isNotEqualTo(new TextRangeWithHash(1, 2, 3, 44, "md5")) .isNotEqualTo(new TextRangeWithHash(1, 2, 3, 4, "md55")) .isNotEqualTo("foo"); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/VersionTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class VersionTests { @Test void test_fields_of_snapshot_versions() { var version = Version.create("1.2.3-SNAPSHOT"); assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(2); assertThat(version.getPatch()).isEqualTo(3); assertThat(version.getBuild()).isEqualTo(0); assertThat(version.getQualifier()).isEqualTo("SNAPSHOT"); } @Test void test_fields_of_releases() { var version = Version.create("1.2"); assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(2); assertThat(version.getPatch()).isEqualTo(0); assertThat(version.getBuild()).isEqualTo(0); assertThat(version.getQualifier()).isEmpty(); } @Test void compare_releases() { var version12 = Version.create("1.2"); var version121 = Version.create("1.2.1"); assertThat(version12) .hasToString("1.2") .isEqualByComparingTo(version12); assertThat(version121) .isEqualByComparingTo(version121) .isGreaterThan(version12); } @Test void compare_snapshots() { var version12 = Version.create("1.2"); var version12Snapshot = Version.create("1.2-SNAPSHOT"); var version121Snapshot = Version.create("1.2.1-SNAPSHOT"); var version12RC = Version.create("1.2-RC1"); assertThat(version12).isGreaterThan(version12Snapshot); assertThat(version12Snapshot).isEqualByComparingTo(version12Snapshot); assertThat(version121Snapshot).isGreaterThan(version12Snapshot); assertThat(version12Snapshot).isGreaterThan(version12RC); } @Test void compare_release_candidates() { var version12 = Version.create("1.2"); var version12Snapshot = Version.create("1.2-SNAPSHOT"); var version12RC1 = Version.create("1.2-RC1"); var version12RC2 = Version.create("1.2-RC2"); assertThat(version12RC1) .isLessThan(version12Snapshot) .isEqualByComparingTo(version12RC1) .isLessThan(version12RC2) .isLessThan(version12); } @Test void testTrim() { var version12 = Version.create(" 1.2 "); assertThat(version12.getName()).isEqualTo("1.2"); assertThat(version12).isEqualTo(Version.create("1.2")); } @Test void testDefaultNumberIsZero() { var version12 = Version.create("1.2"); var version120 = Version.create("1.2.0"); assertThat(version12).isEqualTo(version120); assertThat(version120).isEqualTo(version12); } @Test void testCompareOnTwoDigits() { var version1dot10 = Version.create("1.10"); var version1dot1 = Version.create("1.1"); var version1dot9 = Version.create("1.9"); assertThat(version1dot10) .isGreaterThan(version1dot1) .isGreaterThan(version1dot9); } @Test void testFields() { var version = Version.create("1.10.2"); assertThat(version.getName()).isEqualTo("1.10.2"); assertThat(version).hasToString("1.10.2"); assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(10); assertThat(version.getPatch()).isEqualTo(2); assertThat(version.getBuild()).isZero(); } @Test void testPatchFieldsEquals() { var version = Version.create("1.2.3.4"); assertThat(version.getPatch()).isEqualTo(3); assertThat(version.getBuild()).isEqualTo(4); assertThat(version) .isEqualTo(version) .isEqualTo(Version.create("1.2.3.4")) .isNotEqualTo(Version.create("1.2.3.5")); } @Test void removeQualifier() { var version = Version.create("1.2.3-SNAPSHOT").removeQualifier(); assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(2); assertThat(version.getPatch()).isEqualTo(3); assertThat(version.getQualifier()).isEmpty(); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/log/ConcurrentListAppender.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import ch.qos.logback.core.AppenderBase; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; public class ConcurrentListAppender extends AppenderBase { public final Queue list = new ConcurrentLinkedQueue(); protected void append(E e) { list.add(e); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/log/MessageFormatterTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; /** * @author Ceki Gulcu */ class MessageFormatterTests { Integer i1 = 1; Integer i2 = 2; Integer i3 = 3; Integer[] ia0 = new Integer[] {i1, i2, i3}; Integer[] ia1 = new Integer[] {10, 20, 30}; String result; @Test void testNull() { result = MessageFormatter.format(null, i1).getMessage(); assertNull(result); } @Test void testParamaterContainingAnAnchor() { result = MessageFormatter.format("Value is {}.", "[{}]").getMessage(); assertEquals("Value is [{}].", result); result = MessageFormatter.format("Values are {} and {}.", i1, "[{}]").getMessage(); assertEquals("Values are 1 and [{}].", result); } @Test void nullParametersShouldBeHandledWithoutBarfing() { result = MessageFormatter.format("Value is {}.", null).getMessage(); assertEquals("Value is null.", result); result = MessageFormatter.format("Val1 is {}, val2 is {}.", null, null).getMessage(); assertEquals("Val1 is null, val2 is null.", result); result = MessageFormatter.format("Val1 is {}, val2 is {}.", i1, null).getMessage(); assertEquals("Val1 is 1, val2 is null.", result); result = MessageFormatter.format("Val1 is {}, val2 is {}.", null, i2).getMessage(); assertEquals("Val1 is null, val2 is 2.", result); result = MessageFormatter.arrayFormat("Val1 is {}, val2 is {}, val3 is {}", new Integer[] {null, null, null}).getMessage(); assertEquals("Val1 is null, val2 is null, val3 is null", result); result = MessageFormatter.arrayFormat("Val1 is {}, val2 is {}, val3 is {}", new Integer[] {null, i2, i3}).getMessage(); assertEquals("Val1 is null, val2 is 2, val3 is 3", result); result = MessageFormatter.arrayFormat("Val1 is {}, val2 is {}, val3 is {}", new Integer[] {null, null, i3}).getMessage(); assertEquals("Val1 is null, val2 is null, val3 is 3", result); } @Test void verifyOneParameterIsHandledCorrectly() { result = MessageFormatter.format("Value is {}.", i3).getMessage(); assertEquals("Value is 3.", result); result = MessageFormatter.format("Value is {", i3).getMessage(); assertEquals("Value is {", result); result = MessageFormatter.format("{} is larger than 2.", i3).getMessage(); assertEquals("3 is larger than 2.", result); result = MessageFormatter.format("No subst", i3).getMessage(); assertEquals("No subst", result); result = MessageFormatter.format("Incorrect {subst", i3).getMessage(); assertEquals("Incorrect {subst", result); result = MessageFormatter.format("Value is {bla} {}", i3).getMessage(); assertEquals("Value is {bla} 3", result); result = MessageFormatter.format("Escaped \\{} subst", i3).getMessage(); assertEquals("Escaped {} subst", result); result = MessageFormatter.format("{Escaped", i3).getMessage(); assertEquals("{Escaped", result); result = MessageFormatter.format("\\{}Escaped", i3).getMessage(); assertEquals("{}Escaped", result); result = MessageFormatter.format("File name is {{}}.", "App folder.zip").getMessage(); assertEquals("File name is {App folder.zip}.", result); // escaping the escape character result = MessageFormatter.format("File name is C:\\\\{}.", "App folder.zip").getMessage(); assertEquals("File name is C:\\App folder.zip.", result); } @Test void testTwoParameters() { result = MessageFormatter.format("Value {} is smaller than {}.", i1, i2).getMessage(); assertEquals("Value 1 is smaller than 2.", result); result = MessageFormatter.format("Value {} is smaller than {}", i1, i2).getMessage(); assertEquals("Value 1 is smaller than 2", result); result = MessageFormatter.format("{}{}", i1, i2).getMessage(); assertEquals("12", result); result = MessageFormatter.format("Val1={}, Val2={", i1, i2).getMessage(); assertEquals("Val1=1, Val2={", result); result = MessageFormatter.format("Value {} is smaller than \\{}", i1, i2).getMessage(); assertEquals("Value 1 is smaller than {}", result); result = MessageFormatter.format("Value {} is smaller than \\{} tail", i1, i2).getMessage(); assertEquals("Value 1 is smaller than {} tail", result); result = MessageFormatter.format("Value {} is smaller than \\{", i1, i2).getMessage(); assertEquals("Value 1 is smaller than \\{", result); result = MessageFormatter.format("Value {} is smaller than {tail", i1, i2).getMessage(); assertEquals("Value 1 is smaller than {tail", result); result = MessageFormatter.format("Value \\{} is smaller than {}", i1, i2).getMessage(); assertEquals("Value {} is smaller than 1", result); } @Test void testExceptionIn_toString() { Object o = new Object() { @Override public String toString() { throw new IllegalStateException("a"); } }; result = MessageFormatter.format("Troublesome object {}", o).getMessage(); assertEquals("Troublesome object [FAILED toString()]", result); } @Test void testNullArray() { var msg0 = "msg0"; var msg1 = "msg1 {}"; var msg2 = "msg2 {} {}"; var msg3 = "msg3 {} {} {}"; Object[] args = null; result = MessageFormatter.arrayFormat(msg0, args).getMessage(); assertEquals(msg0, result); result = MessageFormatter.arrayFormat(msg1, args).getMessage(); assertEquals(msg1, result); result = MessageFormatter.arrayFormat(msg2, args).getMessage(); assertEquals(msg2, result); result = MessageFormatter.arrayFormat(msg3, args).getMessage(); assertEquals(msg3, result); } // tests the case when the parameters are supplied in a single array @Test void testArrayFormat() { result = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia0).getMessage(); assertEquals("Value 1 is smaller than 2 and 3.", result); result = MessageFormatter.arrayFormat("{}{}{}", ia0).getMessage(); assertEquals("123", result); result = MessageFormatter.arrayFormat("Value {} is smaller than {}.", ia0).getMessage(); assertEquals("Value 1 is smaller than 2.", result); result = MessageFormatter.arrayFormat("Value {} is smaller than {}", ia0).getMessage(); assertEquals("Value 1 is smaller than 2", result); result = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia0).getMessage(); assertEquals("Val=1, {, Val=2", result); result = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia0).getMessage(); assertEquals("Val=1, {, Val=2", result); result = MessageFormatter.arrayFormat("Val1={}, Val2={", ia0).getMessage(); assertEquals("Val1=1, Val2={", result); } @Test void testArrayValues() { var p0 = i1; var p1 = new Integer[] {i2, i3}; result = MessageFormatter.format("{}{}", p0, p1).getMessage(); assertEquals("1[2, 3]", result); // Integer[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", p1}).getMessage(); assertEquals("a[2, 3]", result); // byte[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new byte[] {1, 2}}).getMessage(); assertEquals("a[1, 2]", result); // int[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new int[] {1, 2}}).getMessage(); assertEquals("a[1, 2]", result); // float[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new float[] {1, 2}}).getMessage(); assertEquals("a[1.0, 2.0]", result); // double[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new double[] {1, 2}}).getMessage(); assertEquals("a[1.0, 2.0]", result); // boolean[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new boolean[] {true, false}}).getMessage(); assertEquals("a[true, false]", result); // short[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new short[] {1, 2}}).getMessage(); assertEquals("a[1, 2]", result); // char[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new char[] {'a', 'b'}}).getMessage(); assertEquals("a[a, b]", result); // long[] result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", new long[] {1, 2}}).getMessage(); assertEquals("a[1, 2]", result); } @Test void testMultiDimensionalArrayValues() { var multiIntegerA = new Integer[][] {ia0, ia1}; result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", multiIntegerA}).getMessage(); assertEquals("a[[1, 2, 3], [10, 20, 30]]", result); var multiIntA = new int[][] {{1, 2}, {10, 20}}; result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", multiIntA}).getMessage(); assertEquals("a[[1, 2], [10, 20]]", result); var multiFloatA = new float[][] {{1, 2}, {10, 20}}; result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", multiFloatA}).getMessage(); assertEquals("a[[1.0, 2.0], [10.0, 20.0]]", result); var multiOA = new Object[][] {ia0, ia1}; result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", multiOA}).getMessage(); assertEquals("a[[1, 2, 3], [10, 20, 30]]", result); var _3DOA = new Object[][][] {multiOA, multiOA}; result = MessageFormatter.arrayFormat("{}{}", new Object[] {"a", _3DOA}).getMessage(); assertEquals("a[[[1, 2, 3], [10, 20, 30]], [[1, 2, 3], [10, 20, 30]]]", result); } @Test void testCyclicArrays() { { var cyclicA = new Object[1]; cyclicA[0] = cyclicA; assertEquals("[[...]]", MessageFormatter.arrayFormat("{}", cyclicA).getMessage()); } { var a = new Object[2]; a[0] = i1; var c = new Object[] {i3, a}; var b = new Object[] {i2, c}; a[1] = b; assertEquals("1[2, [3, [1, [...]]]]", MessageFormatter.arrayFormat("{}{}", a).getMessage()); } } @Test void testArrayThrowable() { FormattingTuple ft; var t = new Throwable(); var ia = new Object[] {i1, i2, i3, t}; ft = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia); assertEquals("Value 1 is smaller than 2 and 3.", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("{}{}{}", ia); assertEquals("123", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Value {} is smaller than {}.", ia); assertEquals("Value 1 is smaller than 2.", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Value {} is smaller than {}", ia); assertEquals("Value 1 is smaller than 2", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Val={}, {, Val={}", ia); assertEquals("Val=1, {, Val=2", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Val={}, \\{, Val={}", ia); assertEquals("Val=1, \\{, Val=2", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Val1={}, Val2={", ia); assertEquals("Val1=1, Val2={", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("Value {} is smaller than {} and {}.", ia); assertEquals("Value 1 is smaller than 2 and 3.", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("{}{}{}{}", ia); assertEquals("123{}", ft.getMessage()); assertEquals(t, ft.getThrowable()); ft = MessageFormatter.arrayFormat("1={}", new Object[] {i1}, t); assertEquals("1=1", ft.getMessage()); assertEquals(t, ft.getThrowable()); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/log/SonarLintLogTester.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import javax.annotation.Nullable; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.LoggerFactory; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; /** * For tests only *
* This JUnit 5 extension allows to access logs in tests. *
* Warning - not compatible with parallel execution of tests in the same JVM fork. *
* Example: *
 * public class MyClass {
 *   private final SonarLintLogger logger = SonarLintLogger.get();
 *
 *   public void doSomething() {
 *     logger.info("foo");
 *   }
 * }
 *
 * class MyClassTests {
 *   @org.junit.jupiter.api.extension.RegisterExtension
 *   SonarLintLogTester logTester = new SonarLintLogTester();
 *
 *   @org.junit.jupiter.api.Test
 *   public void test_log() {
 *     new MyClass().doSomething();
 *
 *     assertThat(logTester.logs()).containsOnly("foo");
 *   }
 * }
 * 
*/ public class SonarLintLogTester implements AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback { private final Queue logs = new ConcurrentLinkedQueue<>(); private final Map> logsByLevel = new ConcurrentHashMap<>(); private final LogOutput logOutput; private final ConcurrentListAppender listAppender = new ConcurrentListAppender<>(); public SonarLintLogTester(boolean writeToStdOut) { logOutput = new LogOutput() { @Override public void log(@Nullable String formattedMessage, Level level, @Nullable String stacktrace) { if (formattedMessage != null) { logs.add(formattedMessage); logsByLevel.computeIfAbsent(level, l -> new ConcurrentLinkedQueue<>()).add(formattedMessage); } if (stacktrace != null) { logs.add(stacktrace); logsByLevel.computeIfAbsent(level, l -> new ConcurrentLinkedQueue<>()).add(stacktrace); } if (writeToStdOut) { System.out.println(level + " " + (formattedMessage != null ? formattedMessage : "")); if (stacktrace != null) { System.out.println(stacktrace); } } } }; } public SonarLintLogTester() { this(false); } @Override public void afterTestExecution(ExtensionContext context) { clear(); } public void clear() { logs.clear(); logsByLevel.clear(); listAppender.list.clear(); } public LogOutput getLogOutput() { return logOutput; } /** * Logs in chronological order (item at index 0 is the oldest one) */ public List logs() { return List.copyOf(logs); } /** * Logs in chronological order (item at index 0 is the oldest one) for * a given level */ public List logs(Level level) { return Optional.ofNullable(logsByLevel.get(level)).map(List::copyOf).orElse(List.of()); } @Override public void afterAll(ExtensionContext context) { SonarLintLogger.get().setTarget(null); listAppender.stop(); listAppender.list.clear(); getRootLogger().detachAppender(listAppender); } @Override public void beforeAll(ExtensionContext context) { SonarLintLogger.get().setLevel(Level.DEBUG); SonarLintLogger.get().setTarget(logOutput); getRootLogger().addAppender(listAppender); listAppender.start(); } private static ch.qos.logback.classic.Logger getRootLogger() { return (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/log/SonarLintLoggerTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.log; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; class SonarLintLoggerTests { private static final NullPointerException THROWN = new NullPointerException(); private final LogOutput output = mock(LogOutput.class); private final SonarLintLogger logger = new SonarLintLogger(); @BeforeEach void prepare() { logger.setLevel(Level.TRACE); logger.setTarget(output); } @Test void should_log_error() { logger.error("msg1"); logger.error("msg", (Object) null); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var emptyArgs = new Object[0]; logger.error("msg", emptyArgs); logger.error("msg {}", "a"); logger.error("msg {} {}", "a", "a"); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var args = new Object[] {"b"}; logger.error("msg {}", args); logger.error("msg with ex", THROWN); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log("msg1", Level.ERROR, null); inOrder.verify(output, times(2)).log("msg", Level.ERROR, null); inOrder.verify(output).log("msg a", Level.ERROR, null); inOrder.verify(output).log("msg a a", Level.ERROR, null); inOrder.verify(output).log("msg b", Level.ERROR, null); inOrder.verify(output).log(eq("msg with ex"), eq(Level.ERROR), argThat(arg -> arg.contains("NullPointerException"))); } @Test void should_log_warn() { logger.warn("msg1"); logger.warn("msg", (Object) null); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var emptyArgs = new Object[0]; logger.warn("msg", emptyArgs); logger.warn("msg {}", "a"); logger.warn("msg {} {}", "a", "a"); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var args = new Object[] {"b"}; logger.warn("msg {}", args); logger.warn("msg with ex", THROWN); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log("msg1", Level.WARN, null); inOrder.verify(output, times(2)).log("msg", Level.WARN, null); inOrder.verify(output).log("msg a", Level.WARN, null); inOrder.verify(output).log("msg a a", Level.WARN, null); inOrder.verify(output).log("msg b", Level.WARN, null); inOrder.verify(output).log(eq("msg with ex"), eq(Level.WARN), argThat(arg -> arg.contains("NullPointerException"))); } @Test void should_log_info() { logger.info("msg1"); logger.info("msg", (Object) null); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var emptyArgs = new Object[0]; logger.info("msg", emptyArgs); logger.info("msg {}", "a"); logger.info("msg {} {}", "a", "a"); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var args = new Object[] {"b"}; logger.info("msg {}", args); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log("msg1", Level.INFO, null); inOrder.verify(output, times(2)).log("msg", Level.INFO, null); inOrder.verify(output).log("msg a", Level.INFO, null); inOrder.verify(output).log("msg a a", Level.INFO, null); inOrder.verify(output).log("msg b", Level.INFO, null); } @Test void should_log_debug() { logger.debug("msg1"); logger.debug("msg", (Object) null); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var emptyArgs = new Object[0]; logger.debug("msg", emptyArgs); logger.debug("msg {}", "a"); logger.debug("msg {} {}", "a", "a"); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var args = new Object[] {"b"}; logger.debug("msg {}", args); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log("msg1", Level.DEBUG, null); inOrder.verify(output, times(2)).log("msg", Level.DEBUG, null); inOrder.verify(output).log("msg a", Level.DEBUG, null); inOrder.verify(output).log("msg a a", Level.DEBUG, null); inOrder.verify(output).log("msg b", Level.DEBUG, null); } @Test void should_log_trace() { logger.trace("msg1"); logger.trace("msg", (Object) null); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var emptyArgs = new Object[0]; logger.trace("msg", emptyArgs); logger.trace("msg {}", "a"); logger.trace("msg {} {}", "a", "a"); // Keep a separate variable to avoid Eclipse refactoring into a non varargs method var args = new Object[] {"b"}; logger.trace("msg {}", args); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log("msg1", Level.TRACE, null); inOrder.verify(output, times(2)).log("msg", Level.TRACE, null); inOrder.verify(output).log("msg a", Level.TRACE, null); inOrder.verify(output).log("msg a a", Level.TRACE, null); inOrder.verify(output).log("msg b", Level.TRACE, null); } // SLCORE-292 @Test void extract_throwable_from_format_params() { var throwable = new Throwable("thrown"); logger.error("msg", (Object) throwable); logger.error("msg {}", "a", throwable); logger.error("msg {} {}", "a", "a", throwable); var inOrder = Mockito.inOrder(output); inOrder.verify(output).log(eq("msg"), eq(Level.ERROR), argThat(arg -> arg.contains("thrown"))); inOrder.verify(output).log(eq("msg a"), eq(Level.ERROR), argThat(arg -> arg.contains("thrown"))); inOrder.verify(output).log(eq("msg a a"), eq(Level.ERROR), argThat(arg -> arg.contains("thrown"))); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/progress/ExecutorServiceShutdownWatchableTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.progress; import java.util.ArrayList; import java.util.concurrent.ExecutorService; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class ExecutorServiceShutdownWatchableTests { @Test void should_cancel_all_monitors() { ExecutorServiceShutdownWatchable underTest = new ExecutorServiceShutdownWatchable<>(mock(ExecutorService.class)); var monitors = new ArrayList(); for (int i = 0; i < 1000; i++) { var monitor = new SonarLintCancelMonitor(); underTest.cancelOnShutdown(monitor); monitors.add(monitor); } underTest.shutdown(); assertThat(monitors).allMatch(SonarLintCancelMonitor::isCanceled); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/storage/DatabaseExceptionReporterTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import io.sentry.IScope; import io.sentry.Sentry; import io.sentry.ScopeCallback; import io.sentry.logger.ILoggerApi; import java.sql.SQLException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.verify; class DatabaseExceptionReporterTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private MockedStatic sentryMock; @BeforeEach void setUp() { DatabaseExceptionReporter.clearRecentExceptions(); sentryMock = mockStatic(Sentry.class); sentryMock.when(Sentry::logger).thenReturn(mock(ILoggerApi.class)); } @AfterEach void tearDown() { sentryMock.close(); DatabaseExceptionReporter.clearRecentExceptions(); System.clearProperty(DatabaseExceptionReporter.DEDUP_WINDOW_PROPERTY); } @Test void should_capture_generic_exception() { var exception = new RuntimeException("Test database error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", "SELECT * FROM test"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(exception); assertThat(exceptionCaptor.getValue().getMessage()).isEqualTo("Test database error"); } @Test void should_capture_sql_exception_with_details() { var sqlException = new SQLException("SQL error", "42000", 1234); DatabaseExceptionReporter.capture(sqlException, "startup", "flyway.migrate", null); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isInstanceOf(SQLException.class); assertThat(((SQLException) exceptionCaptor.getValue()).getSQLState()).isEqualTo("42000"); } @Test void should_capture_exception_without_sql() { var exception = new RuntimeException("Pool creation failed"); DatabaseExceptionReporter.capture(exception, "startup", "h2.pool.create"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(exception); } @Test void should_deduplicate_same_message_within_window() { var exception = new RuntimeException("Duplicate error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", "SELECT 1"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", "SELECT 1"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class)), times(1)); assertThat(exceptionCaptor.getValue()).isSameAs(exception); } @Test void should_not_deduplicate_different_exceptions() { var exception1 = new RuntimeException("Error 1"); var exception2 = new RuntimeException("Error 2"); DatabaseExceptionReporter.capture(exception1, "runtime", "jooq.execute", "SELECT 1"); DatabaseExceptionReporter.capture(exception2, "runtime", "jooq.execute", "SELECT 2"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class)), times(2)); assertThat(exceptionCaptor.getAllValues()).containsExactly(exception1, exception2); } @Test void should_deduplicate_same_message_even_with_different_phase() { var exception = new RuntimeException("Same error"); DatabaseExceptionReporter.capture(exception, "startup", "h2.pool.create"); DatabaseExceptionReporter.capture(exception, "shutdown", "h2.pool.dispose"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class)), times(1)); assertThat(exceptionCaptor.getValue()).isSameAs(exception); } @Test void should_always_report_null_message_exceptions_without_deduplication() { var exception1 = new RuntimeException(); var exception2 = new RuntimeException(); DatabaseExceptionReporter.capture(exception1, "runtime", "jooq.execute"); DatabaseExceptionReporter.capture(exception2, "runtime", "jooq.execute"); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class)), times(2)); assertThat(exceptionCaptor.getAllValues()).containsExactly(exception1, exception2); } @Test void should_truncate_long_sql() { var longSql = "SELECT " + "a".repeat(2000) + " FROM test"; var exception = new RuntimeException("SQL error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", longSql); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(exception); } @Test void should_cleanup_old_entries_after_dedup_window() { System.setProperty(DatabaseExceptionReporter.DEDUP_WINDOW_PROPERTY, "50"); DatabaseExceptionReporter.capture(new RuntimeException("Error 1"), "runtime", "op1"); DatabaseExceptionReporter.capture(new RuntimeException("Error 2"), "runtime", "op2"); DatabaseExceptionReporter.capture(new RuntimeException("Error 3"), "runtime", "op3"); assertThat(DatabaseExceptionReporter.getRecentExceptionsCount()).isEqualTo(3); await().atLeast(java.time.Duration.ofMillis(100)).untilAsserted(() -> { DatabaseExceptionReporter.capture(new RuntimeException("Error 4"), "runtime", "op4"); assertThat(DatabaseExceptionReporter.getRecentExceptionsCount()).isEqualTo(1); }); } @Test void should_set_scope_tags_for_generic_exception() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var exception = new RuntimeException("Test error"); DatabaseExceptionReporter.capture(exception, "startup", "h2.pool.create"); verify(scope).setTag("component", "database"); verify(scope).setTag("db.phase", "startup"); verify(scope).setTag("db.operation", "h2.pool.create"); } @Test void should_set_scope_tags_for_sql_exception_with_sql_state() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var sqlException = new SQLException("SQL error", "42000", 1234); DatabaseExceptionReporter.capture(sqlException, "runtime", "jooq.execute"); verify(scope).setTag("component", "database"); verify(scope).setTag("db.phase", "runtime"); verify(scope).setTag("db.operation", "jooq.execute"); verify(scope).setTag("db.sqlState", "42000"); verify(scope).setTag("db.errorCode", "1234"); } @Test void should_not_set_sql_state_tag_when_null() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var sqlException = new SQLException("SQL error", null, 5678); DatabaseExceptionReporter.capture(sqlException, "runtime", "jooq.execute"); verify(scope).setTag("component", "database"); verify(scope).setTag("db.errorCode", "5678"); verify(scope, times(0)).setTag("db.sqlState", null); } @Test void should_set_sql_extra_when_provided() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var exception = new RuntimeException("Test error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", "SELECT * FROM test"); verify(scope).setExtra("db.sql", "SELECT * FROM test"); } @Test void should_not_set_sql_extra_when_empty() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var exception = new RuntimeException("Test error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", ""); verify(scope, times(0)).setExtra(any(), any()); } @Test void should_truncate_sql_in_extra_when_exceeds_1000_chars() { var scope = mock(IScope.class); sentryMock.when(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class))) .thenAnswer(invocation -> { var callback = invocation.getArgument(1, ScopeCallback.class); callback.run(scope); return null; }); var longSql = "SELECT " + "a".repeat(2000) + " FROM test"; var exception = new RuntimeException("SQL error"); DatabaseExceptionReporter.capture(exception, "runtime", "jooq.execute", longSql); var sqlCaptor = ArgumentCaptor.forClass(String.class); verify(scope).setExtra(any(), sqlCaptor.capture()); assertThat(sqlCaptor.getValue()).hasSize(1000 + "... [truncated]".length()); assertThat(sqlCaptor.getValue()).endsWith("... [truncated]"); } @Test void should_use_default_dedup_window_when_property_is_invalid() { System.setProperty(DatabaseExceptionReporter.DEDUP_WINDOW_PROPERTY, "not-a-number"); var exception1 = new RuntimeException("Error"); var exception2 = new RuntimeException("Error"); DatabaseExceptionReporter.capture(exception1, "runtime", "op1"); DatabaseExceptionReporter.capture(exception2, "runtime", "op2"); // Should still deduplicate using default window (not crash) sentryMock.verify(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class)), times(1)); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/storage/JooqDatabaseExceptionListenerTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import io.sentry.Sentry; import io.sentry.ScopeCallback; import io.sentry.logger.ILoggerApi; import java.sql.SQLException; import org.jooq.ExecuteContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class JooqDatabaseExceptionListenerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private JooqDatabaseExceptionListener listener; private MockedStatic sentryMock; @BeforeEach void setUp() { listener = new JooqDatabaseExceptionListener(); DatabaseExceptionReporter.clearRecentExceptions(); sentryMock = mockStatic(Sentry.class); sentryMock.when(Sentry::logger).thenReturn(mock(ILoggerApi.class)); } @AfterEach void tearDown() { sentryMock.close(); DatabaseExceptionReporter.clearRecentExceptions(); } @Test void should_report_sql_exception_from_context() { var ctx = mock(ExecuteContext.class); var sqlException = new SQLException("SQL syntax error", "42000", 1064); var runtimeException = new RuntimeException("Wrapped exception", sqlException); when(ctx.exception()).thenReturn(runtimeException); when(ctx.sqlException()).thenReturn(sqlException); when(ctx.sql()).thenReturn("SELECT * FROM invalid_table"); listener.exception(ctx); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isInstanceOf(SQLException.class); assertThat(((SQLException) exceptionCaptor.getValue()).getSQLState()).isEqualTo("42000"); } @Test void should_report_runtime_exception_when_no_sql_exception() { var ctx = mock(ExecuteContext.class); var runtimeException = new RuntimeException("jOOQ execution failed"); when(ctx.exception()).thenReturn(runtimeException); when(ctx.sqlException()).thenReturn(null); when(ctx.sql()).thenReturn("INSERT INTO test VALUES (1)"); listener.exception(ctx); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(runtimeException); } @Test void should_not_report_when_no_exception() { var ctx = mock(ExecuteContext.class); when(ctx.exception()).thenReturn(null); listener.exception(ctx); sentryMock.verify(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class)), never()); } @Test void should_handle_null_sql() { var ctx = mock(ExecuteContext.class); var exception = new RuntimeException("Error without SQL"); when(ctx.exception()).thenReturn(exception); when(ctx.sqlException()).thenReturn(null); when(ctx.sql()).thenReturn(null); listener.exception(ctx); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(exception); } @Test void should_prefer_sql_exception_over_runtime_exception() { var ctx = mock(ExecuteContext.class); var sqlException = new SQLException("Constraint violation", "23000", 1062); var runtimeException = new RuntimeException("Wrapper", sqlException); when(ctx.exception()).thenReturn(runtimeException); when(ctx.sqlException()).thenReturn(sqlException); when(ctx.sql()).thenReturn("INSERT INTO test (id) VALUES (1)"); listener.exception(ctx); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); assertThat(exceptionCaptor.getValue()).isSameAs(sqlException); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/storage/SonarLintDatabaseExceptionTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage; import io.sentry.Sentry; import io.sentry.ScopeCallback; import io.sentry.logger.ILoggerApi; import java.nio.file.Path; import java.sql.SQLException; import org.jooq.exception.DataAccessException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; class SonarLintDatabaseExceptionTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private SonarLintDatabase db; private MockedStatic sentryMock; @BeforeEach void setUp() { DatabaseExceptionReporter.clearRecentExceptions(); sentryMock = mockStatic(Sentry.class); sentryMock.when(Sentry::logger).thenReturn(mock(ILoggerApi.class)); } @AfterEach void tearDown() { if (db != null) { db.shutdown(); } sentryMock.close(); DatabaseExceptionReporter.clearRecentExceptions(); } @Test void should_report_runtime_sql_exception_via_listener(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); assertThatThrownBy(() -> db.dsl().execute("SELECT * FROM non_existent_table")) .isInstanceOf(DataAccessException.class); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); var capturedException = exceptionCaptor.getValue(); assertThat(capturedException).isInstanceOf(SQLException.class); var sqlException = (SQLException) capturedException; assertThat(sqlException.getSQLState()).isEqualTo("42S02"); assertThat(sqlException.getMessage()).contains("NON_EXISTENT_TABLE"); } @Test void should_report_invalid_sql_syntax_exception(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); assertThatThrownBy(() -> db.dsl().execute("INVALID SQL SYNTAX HERE")) .isInstanceOf(DataAccessException.class); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); var capturedException = exceptionCaptor.getValue(); assertThat(capturedException).isInstanceOf(SQLException.class); var sqlException = (SQLException) capturedException; assertThat(sqlException.getSQLState()).isEqualTo("42001"); } @Test void should_report_constraint_violation_exception(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); db.dsl().execute("CREATE TABLE IF NOT EXISTS test_table (id INT PRIMARY KEY, name VARCHAR(100))"); db.dsl().execute("INSERT INTO test_table (id, name) VALUES (1, 'test')"); assertThatThrownBy(() -> db.dsl().execute("INSERT INTO test_table (id, name) VALUES (1, 'duplicate')")) .isInstanceOf(DataAccessException.class); var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); sentryMock.verify(() -> Sentry.captureException(exceptionCaptor.capture(), any(ScopeCallback.class))); var capturedException = exceptionCaptor.getValue(); assertThat(capturedException).isInstanceOf(SQLException.class); var sqlException = (SQLException) capturedException; assertThat(sqlException.getSQLState()).isEqualTo("23505"); assertThat(sqlException.getMessage()).contains("Unique index or primary key violation"); } @Test void should_initialize_database_successfully(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); assertThat(db.dsl()).isNotNull(); sentryMock.verify(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class)), never()); } @Test void should_shutdown_database_successfully(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); db.shutdown(); db = null; sentryMock.verify(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class)), never()); } @Test void should_execute_valid_queries_without_exception_reporting(@TempDir Path tempDir) { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); db.dsl().execute("CREATE TABLE IF NOT EXISTS valid_table (id INT, name VARCHAR(100))"); db.dsl().execute("INSERT INTO valid_table (id, name) VALUES (1, 'test')"); var result = db.dsl().fetch("SELECT * FROM valid_table WHERE id = 1"); assertThat(result).hasSize(1); sentryMock.verify(() -> Sentry.captureException(any(Throwable.class), any(ScopeCallback.class)), never()); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/storage/local/FileStorageManagerTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.storage.local; import com.google.gson.GsonBuilder; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.storage.adapter.LocalDateAdapter; import org.sonarsource.sonarlint.core.commons.storage.adapter.LocalDateTimeAdapter; import org.sonarsource.sonarlint.core.commons.storage.adapter.OffsetDateTimeAdapter; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.*; class FileStorageManagerTest { private Path filePath; @BeforeEach void setUp(@TempDir Path temp) { filePath = temp.resolve("test"); } @Test void should_update() { var storage = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); storage.getStorage(); assertThat(filePath).doesNotExist(); storage.tryUpdateAtomically(dummy -> dummy.data = "update"); assertThat(filePath).exists(); var dummy = storage.getStorage(); assertThat(dummy.data).isEqualTo("update"); } @Test void supportConcurrentUpdates() { var storage = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); int nThreads = 10; var executorService = Executors.newFixedThreadPool(nThreads); CountDownLatch latch = new CountDownLatch(1); List> futures = new ArrayList<>(); // Each thread will attempt to increment the counter by one IntStream.range(0, nThreads).forEach(i -> { futures.add(executorService.submit(() -> { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } storage.tryUpdateAtomically(data -> data.counter++); })); }); latch.countDown(); futures.forEach(f -> { try { f.get(); } catch (ExecutionException e) { fail(e.getCause()); } catch (InterruptedException e) { e.printStackTrace(); } }); assertThat(storage.getStorage().counter).isEqualTo(nThreads); } @Test void tryUpdateAtomically_should_not_crash_if_too_many_read_write_requests() { var storageManager = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); Runnable read = () -> storageManager.getStorage().getCounter(); Runnable write = () -> storageManager.tryUpdateAtomically(dummy -> dummy.counter++); Stream.of( IntStream.range(0, 100).mapToObj(operand -> CompletableFuture.runAsync(write)), IntStream.range(0, 100).mapToObj(value -> CompletableFuture.runAsync(read)), IntStream.range(0, 100).mapToObj(operand -> CompletableFuture.runAsync(write)), IntStream.range(0, 100).mapToObj(value -> CompletableFuture.runAsync(read)) ).flatMap(Function.identity()) .forEach(CompletableFuture::join); assertThat(storageManager.getStorage().counter).isEqualTo(200); } @Test void tryRead_should_be_aware_of_file_deletion() { var storageManager = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); assertThat(storageManager.getStorage().counter).isZero(); storageManager.tryUpdateAtomically(dummy -> dummy.counter++); assertThat(storageManager.getStorage().counter).isEqualTo(1); filePath.toFile().delete(); assertThat(storageManager.getStorage().counter).isZero(); } /** * Disabled on Windows because it doesn't always give the file modification time correctly */ @Test @DisabledOnOs(OS.WINDOWS) void tryRead_should_be_aware_of_file_modification() throws IOException { var storageManager = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); assertThat(storageManager.getStorage().counter).isZero(); storageManager.tryUpdateAtomically(dummy -> dummy.counter++); assertThat(storageManager.getStorage().counter).isEqualTo(1); var dummy = new Dummy(); dummy.counter = 2; writeToLocalStorageFile(dummy); await().atMost(5, SECONDS).untilAsserted(() -> assertThat(storageManager.getStorage().counter).isEqualTo(2)); } private void writeToLocalStorageFile(Object newStorage) throws IOException { var newJson = new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter().nullSafe()) .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe()) .create().toJson(newStorage); var encoded = Base64.getEncoder().encode(newJson.getBytes(StandardCharsets.UTF_8)); writeToLocalStorageFile(encoded); } private void writeToLocalStorageFile(byte[] encoded) throws IOException { FileUtils.writeByteArrayToFile(filePath.toFile(), encoded); } @Test void tryRead_returns_default_local_storage_if_file_is_empty() throws IOException { writeToLocalStorageFile(new byte[0]); assertThat(filePath.toFile()).isEmpty(); var storageManager = new FileStorageManager<>(filePath, Dummy::new, Dummy.class); assertThat(storageManager.getStorage().data).isEqualTo("default"); assertThat(storageManager.getStorage().counter).isZero(); } private static class Dummy implements LocalStorage { private String data; private int counter = 0; Dummy() { this("default"); } Dummy(String data) { this.data = data; } public int getCounter() { return counter; } } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/testutils/GitUtils.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.testutils; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Instant; import java.time.ZoneId; import org.apache.commons.io.FilenameUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.util.SystemReader; public class GitUtils { private GitUtils() { // Utils class } public static Git createRepository(Path worktree) throws GitAPIException, IOException { var repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile()); repo.create(); var git = new Git(repo); createEmptyGitIgnoreFile(git); return git; } private static void createEmptyGitIgnoreFile(Git git) throws GitAPIException, IOException { var gitIgnoreFile = getGitIgnoreFile(git); if (gitIgnoreFile.createNewFile()) { git.add().addFilepattern(Constants.GITIGNORE_FILENAME); git.commit().setMessage("Add empty .gitignore").call(); } } public static void addFileToGitIgnoreAndCommit(Git git, String filePath) throws IOException, GitAPIException { var gitIgnoreFile = getGitIgnoreFile(git); // Append the file path to the .gitignore file try (var writer = new FileWriter(gitIgnoreFile, true)) { writer.write("\n" + filePath + "\n"); } commit(git, gitIgnoreFile.getPath()); } private static File getGitIgnoreFile(Git git) { return new File(git.getRepository().getDirectory().getParent(), Constants.GITIGNORE_FILENAME); } public static Instant commit(Git git, String... paths) throws GitAPIException { return commit(git, Instant.now(), paths); } public static Instant commit(Git git, Instant commitDate, String... paths) throws GitAPIException { return commitObject(git, commitDate, paths).getCommitterIdent().getWhenAsInstant(); } private static RevCommit commitObject(Git git, Instant commitDate, String... paths) throws GitAPIException { if (paths.length > 0) { var add = git.add(); for (String p : paths) { add.addFilepattern(FilenameUtils.separatorsToUnix(p)); } add.call(); } return git.commit().setCommitter(new PersonIdent("joe", "email@email.com", commitDate, ZoneId.systemDefault())).setMessage("msg").call(); } public static Instant commitAtDate(Git git, Instant commitDate, String... paths) throws GitAPIException { if (paths.length > 0) { var add = git.add(); for (String p : paths) { add.addFilepattern(FilenameUtils.separatorsToUnix(p)); } add.call(); } var commit = git.commit() .setCommitter(new PersonIdent("joe", "email@email.com", commitDate, SystemReader.getInstance().getTimeZoneAt(commitDate))) .setMessage("msg") .call(); return commit.getCommitterIdent().getWhenAsInstant(); } public static void createFile(Path worktree, String relativePath, String... lines) throws IOException { var newFile = worktree.resolve(relativePath); Files.createDirectories(newFile.getParent()); var content = String.join(System.lineSeparator(), lines) + System.lineSeparator(); Files.write(newFile, content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } public static void appendFile(Path file, String... lines) throws IOException { var content = String.join(System.lineSeparator(), lines) + System.lineSeparator(); Files.write(file, content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND); } public static void modifyFile(Path file, String... lines) throws IOException { var content = String.join(System.lineSeparator(), lines) + System.lineSeparator(); Files.write(file, content.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/testutils/MockWebServerExtension.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.testutils; import java.io.IOException; import java.util.HashMap; import java.util.Map; import mockwebserver3.Dispatcher; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import mockwebserver3.RecordedRequest; import okio.Buffer; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.fail; public class MockWebServerExtension implements BeforeEachCallback, AfterEachCallback { private MockWebServer server; protected final Map responsesByPath = new HashMap<>(); @Override public void beforeEach(ExtensionContext context) { start(); // Most test cases have a call to this endpoint when initializing the data, to decide whether to use Bearer or Basic scheme addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"99.9\",\"status\": \"UP\"}"); } public void start() { server = new MockWebServer(); responsesByPath.clear(); final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { if (responsesByPath.containsKey(request.getPath())) { return responsesByPath.get(request.getPath()); } return new MockResponse.Builder().code(404).build(); } }; server.setDispatcher(dispatcher); try { server.start(); } catch (IOException e) { throw new IllegalStateException("Cannot start the mock web server", e); } } @Override public void afterEach(ExtensionContext context) { shutdown(); } public void shutdown() { try { server.shutdown(); } catch (IOException e) { throw new IllegalStateException("Cannot stop the mock web server", e); } } public void addStringResponse(String path, String body) { responsesByPath.put(path, new MockResponse.Builder().body(body).build()); } public void removeResponse(String path) { responsesByPath.remove(path); } public void addResponse(String path, MockResponse response) { responsesByPath.put(path, response); } public int getRequestCount() { return server.getRequestCount(); } public RecordedRequest takeRequest() { try { return server.takeRequest(); } catch (InterruptedException e) { fail(e); return null; // appeasing the compiler: this line will never be executed. } } public String url(String path) { return server.url(path).toString(); } public void addResponseFromResource(String path, String responseResourcePath) { try (var b = new Buffer()) { responsesByPath.put(path, new MockResponse.Builder().body(b.readFrom(requireNonNull(MockWebServerExtension.class.getResourceAsStream(responseResourcePath)))).build()); } catch (IOException e) { fail(e); } } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/util/git/BlameParserTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.time.Instant; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; class BlameParserTests { @Test void shouldNotPopulateGitBlameResultForEmptyBlameOutput() { var gitBlameReader = new GitBlameReader(); gitBlameReader.readLine(""); assertThat(gitBlameReader.getResult().lineCommitDates()) .isEmpty(); } @Test void shouldSplitBlameOutputCorrectlyWhenLinesContainSplitPattern() { var blameOutput = """ 5746f09bf53067450843eaddff52ea7b0f16cde3 1 1 2 author Some One author-mail author-time 1553598120 author-tz +0100 committer Some One committer-mail committer-time 1554191055 committer-tz +0200 summary Initial revision previous 35c9ca0b1f41231508e706707d76ca0485b8a3ad file.txt filename file.txt First line with filename in it 5746f09bf53067450843eaddff52ea7b0f16cde3 2 2 author Some One author-mail author-time 1553598120 author-tz +0100 committer Some One committer-mail committer-time 1554191057 committer-tz +0200 summary Initial revision previous 35c9ca0b1f41231508e706707d76ca0485b8a3ad file.txt filename file.txt Second line also with filename in it """; var gitBlameReader = new GitBlameReader(); var blameLines = blameOutput.split("\\n"); for (String blameLine : blameLines) { gitBlameReader.readLine(blameLine); } assertThat(gitBlameReader.getResult().lineCommitDates()) .containsExactly(Instant.ofEpochSecond(1554191055), Instant.ofEpochSecond(1554191057)); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/util/git/GitServiceTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.URIish; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.LogTestStartAndEnd; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static java.util.function.Predicate.not; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.jgit.lib.Constants.GITIGNORE_FILENAME; import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.addFileToGitIgnoreAndCommit; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.commit; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createFile; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createRepository; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.modifyFile; import static org.sonarsource.sonarlint.core.commons.util.git.GitService.blameWithGitFilesBlameLibrary; import static org.sonarsource.sonarlint.core.commons.util.git.GitService.getVCSChangedFiles; @ExtendWith(LogTestStartAndEnd.class) class GitServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final NativeGitLocator REAL_NATIVE_GIT_LOCATOR = new NativeGitLocator(); private static final GitService underTest = new GitService(REAL_NATIVE_GIT_LOCATOR); private static Path bareRepoPath; private static Path workingRepoPath; @TempDir private Path projectDirPath; private Git git; @BeforeAll static void beforeAll() throws GitAPIException, IOException { setUpBareRepo(Map.of( ".gitignore", "*.log\n*.tmp\n", "fileA", "lineA1\nlineA2\n", "fileB", "lineB1\nlineB2\n" )); } @AfterAll static void afterAll() { try { FileUtils.forceDelete(bareRepoPath.toFile()); FileUtils.forceDelete(workingRepoPath.toFile()); } catch (Exception ignored) { //It throws an exception in windows } } private static void setUpBareRepo(Map filePathContentMap) throws IOException, GitAPIException { bareRepoPath = Files.createTempDirectory("bare-repo"); workingRepoPath = Files.createTempDirectory("working-repo"); // Initialize a bare repository try (var ignored = Git.init().setBare(true).setDirectory(bareRepoPath.toFile()).call()) { // Initialize a working directory repository try (var workingGit = Git.init().setDirectory(workingRepoPath.toFile()).call()) { // Create a .gitignore file in the working directory for (var filePath : filePathContentMap.keySet()) { var gitignoreFile = new File(workingRepoPath.toFile(), filePath); Files.writeString(gitignoreFile.toPath(), filePathContentMap.get(filePath)); // Stage and commit the .gitignore file workingGit.add().addFilepattern(filePath).call(); workingGit.commit().setMessage("Add " + filePath).call(); } // Add the bare repository as a remote and push the commit workingGit.remoteAdd() .setName("origin") .setUri(new URIish(bareRepoPath.toUri().toString())) .call(); workingGit.push().setRemote("origin").call(); } catch (Exception e) { throw new RuntimeException(e); } } } @BeforeEach void prepare() throws Exception { git = createRepository(projectDirPath); } @AfterEach void cleanup() throws IOException { org.eclipse.jgit.util.FileUtils.delete(projectDirPath.toFile(), RECURSIVE); } @Test void it_should_blame_file() throws IOException, GitAPIException { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); var c1 = commit(git, "fileA"); var sonarLintBlameResult = blameWithGitFilesBlameLibrary(projectDirPath, Set.of(Path.of("fileA")), null); assertThat(IntStream.of(1, 2, 3) .mapToObj(lineNumber -> sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(lineNumber)))) .map(Optional::get) .allMatch(date -> date.equals(c1)); } @Test void it_should_not_blame_new_file() throws IOException { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); var fileAPath = projectDirPath.resolve("fileA"); var filePaths = Set.of(fileAPath); var fileUris = Set.of(fileAPath.toUri()); var now = Instant.now(); var blameResult = underTest.getBlameResult(projectDirPath, filePaths, fileUris, path -> "", now); assertThat(blameResult.getLatestChangeDateForLinesInFile(fileAPath, List.of(1))).isEmpty(); } @Test void it_should_fallback_to_jgit_blame() throws IOException, GitAPIException { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); var c1 = commit(git, "fileA"); var locator = mock(NativeGitLocator.class); when(locator.getNativeGitExecutable()).thenReturn(Optional.empty()); var service = new GitService(locator); var sonarLintBlameResult = service.getBlameResult(projectDirPath, Set.of(Path.of("fileA")), Set.of(Path.of("fileA").toUri()), null, Instant.now()); assertThat(IntStream.of(1, 2, 3) .mapToObj(lineNumber -> sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(lineNumber)))) .map(Optional::get) .allMatch(date -> date.equals(c1)); } @Test void it_should_blame_with_given_contents_within_inner_dir() throws IOException, GitAPIException { var deepFilePath = Path.of("innerDir").resolve("fileA").toString(); createFile(projectDirPath, deepFilePath, "SonarQube", "SonarCloud", "SonarLint"); var c1 = commit(git, deepFilePath); var content = String.join(System.lineSeparator(), "SonarQube", "Cloud", "SonarLint", "SonarSolution") + System.lineSeparator(); UnaryOperator fileContentProvider = path -> deepFilePath.equals(path) ? content : null; var sonarLintBlameResult = blameWithGitFilesBlameLibrary(projectDirPath, Set.of(Path.of(deepFilePath)), fileContentProvider); assertThat(IntStream.of(1, 2, 3, 4) .mapToObj(lineNumber -> sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of(deepFilePath), List.of(lineNumber)))) .map(dateOpt -> dateOpt.orElse(null)) .containsExactly(c1, null, c1, null); } @Test void it_should_blame_file_within_inner_dir() throws IOException, GitAPIException { var deepFilePath = Path.of("innerDir").resolve("fileA").toString(); createFile(projectDirPath, deepFilePath, "line1", "line2", "line3"); var c1 = commit(git, deepFilePath); var sonarLintBlameResult = blameWithGitFilesBlameLibrary(projectDirPath, Set.of(Path.of(deepFilePath)), null); var latestChangeDate = sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of(deepFilePath), List.of(1, 2, 3)); assertThat(latestChangeDate).isPresent().contains(c1); } @Test void it_should_blame_project_files_when_project_base_is_sub_folder_of_git_repo() throws IOException, GitAPIException { projectDirPath = projectDirPath.resolve("subFolder"); createFile(projectDirPath, "fileA", "line1", "line2", "line3"); var c1 = commit(git, git.getRepository().getWorkTree().toPath().relativize(projectDirPath).resolve("fileA").toString()); var sonarLintBlameResult = blameWithGitFilesBlameLibrary(projectDirPath, Set.of(Path.of("fileA")), null); assertThat(IntStream.of(1, 2, 3) .mapToObj(lineNumber -> sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(lineNumber)))) .map(Optional::get) .allMatch(date -> date.equals(c1)); } @Test void it_should_get_uncommitted_files_including_untracked_ones() throws GitAPIException, IOException { var committedFile = "committedFile"; var committedAndModifiedFile = "committedAndModifiedFile"; var uncommittedTrackedFile = "uncommittedTrackedFile"; var uncommittedUntrackedFile = "uncommittedUntrackedFile"; var committedFileUri = projectDirPath.resolve(committedFile).toUri(); var committedAndModifiedFileUri = projectDirPath.resolve(committedAndModifiedFile).toUri(); var uncommittedTrackedFileUri = projectDirPath.resolve(uncommittedTrackedFile).toUri(); var uncommittedUntrackedFileUri = projectDirPath.resolve(uncommittedUntrackedFile).toUri(); var folderFile = Path.of("folder").resolve("folderFile"); var string = FilenameUtils.separatorsToUnix(folderFile.toString()); createFile(projectDirPath, string, "line1", "line2", "line3"); git.add().setUpdate(true).addFilepattern(string).call(); createFile(projectDirPath, committedFile, "line1", "line2", "line3"); commit(git, committedFile); createFile(projectDirPath, committedAndModifiedFile, "line1", "line2", "line3"); commit(git, committedAndModifiedFile); modifyFile(projectDirPath.resolve(committedAndModifiedFile), "line1", "line2", "line3", "line4"); createFile(projectDirPath, uncommittedTrackedFile, "line1", "line2", "line3"); git.add().addFilepattern(uncommittedTrackedFile).call(); createFile(projectDirPath, uncommittedUntrackedFile, "line1", "line2", "line3"); var changedFiles = getVCSChangedFiles(projectDirPath); assertThat(changedFiles).hasSize(4) .doesNotContain(committedFileUri) .contains(committedAndModifiedFileUri) .contains(uncommittedTrackedFileUri) .contains(uncommittedUntrackedFileUri) .contains(projectDirPath.resolve(folderFile).toUri()); } @Test void it_should_get_uncommited_file_in_sub_base_dir() throws GitAPIException, IOException { var folderFile = Path.of("folder").resolve("folderFile"); var string = FilenameUtils.separatorsToUnix(folderFile.toString()); createFile(projectDirPath, string, "line1", "line2", "line3"); git.add().setUpdate(true).addFilepattern(string).call(); var changedFiles = getVCSChangedFiles(projectDirPath.resolve("folder")); assertThat(changedFiles).hasSize(1) .contains(projectDirPath.resolve(folderFile).toUri()); } @Test void it_should_return_empty_list_if_base_dir_not_resolved() { assertThat(getVCSChangedFiles(null)).isEmpty(); } @Test void it_should_return_empty_list_on_git_exception(@TempDir Path nonGitDir) { assertThat(getVCSChangedFiles(nonGitDir)).isEmpty(); } @Test void should_filter_ignored_files() throws IOException, GitAPIException { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); createFile(projectDirPath, "fileB", "line1", "line2", "line3"); createFile(projectDirPath, "fileC", "line1", "line2", "line3"); var fileAPath = Path.of("fileA"); var fileBPath = Path.of("fileB"); var fileCPath = Path.of("fileC"); var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(Stream.of(fileAPath, fileBPath, fileCPath).filter(not(sonarLintGitIgnore::isFileIgnored)).toList()) .hasSize(3) .containsExactly(fileAPath, fileBPath, fileCPath); addFileToGitIgnoreAndCommit(git, "fileB"); sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(Stream.of(fileAPath, fileBPath, fileCPath).filter(not(sonarLintGitIgnore::isFileIgnored)).toList()) .hasSize(2) .containsExactly(fileAPath, fileCPath); } @Test void should_filter_ignored_directories() throws IOException, GitAPIException { var fileA = Path.of("fileA"); var fileB = Path.of("myDir").resolve("fileB"); var fileC = Path.of("myDir").resolve("fileC"); createFile(projectDirPath, "fileA", "line1", "line2", "line3"); createFile(projectDirPath, fileB.toString(), "line1", "line2", "line3"); createFile(projectDirPath, fileC.toString(), "line1", "line2", "line3"); var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(Stream.of(fileA, fileB, fileC).filter(not(sonarLintGitIgnore::isFileIgnored)).toList()) .hasSize(3) .containsExactly(fileA, fileB, fileC); addFileToGitIgnoreAndCommit(git, "myDir/"); sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(Stream.of(fileA, fileB, fileC).filter(not(sonarLintGitIgnore::isFileIgnored)).toList()) .hasSize(1) .containsExactly(fileA); } @Test void should_consider_all_files_not_ignored_on_gitignore() throws IOException { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); createFile(projectDirPath, "fileB", "line1", "line2", "line3"); createFile(projectDirPath, "fileC", "line1", "line2", "line3"); var fileAPath = projectDirPath.resolve("fileA"); var fileBPath = projectDirPath.resolve("fileB"); var fileCPath = projectDirPath.resolve("fileC"); var gitIgnore = projectDirPath.resolve(GITIGNORE_FILENAME); FileUtils.deleteQuietly(gitIgnore.toFile()); var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(logTester.logs(LogOutput.Level.INFO)) .anyMatch(s -> s.contains(".gitignore file was not found for ")); assertThat(Stream.of(fileAPath, fileBPath, fileCPath).filter(not(sonarLintGitIgnore::isFileIgnored)).toList()) .hasSize(3) .containsExactly(fileAPath, fileBPath, fileCPath); } @Test void should_continue_normally_with_null_basedir() { var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(null); assertThat(sonarLintGitIgnore.isIgnored(Path.of("file/path"))).isFalse(); } @Test void should_consider_files_ignored_when_git_root_above_project_root() throws IOException, GitAPIException { var gitRoot = Files.createTempDirectory("test"); var projectRoot = Files.createDirectory(gitRoot.resolve("toto")); try (var ignored = Git.init().setDirectory(gitRoot.toFile()).call()) { var gitignoreFile = new File(gitRoot.toFile(), ".gitignore"); Files.writeString(gitignoreFile.toPath(), "*.js"); } var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectRoot); assertThat(sonarLintGitIgnore.isIgnored(Path.of("frontend/app/should_not_be_ignored.js"))).isTrue(); } @Test void should_respect_gitignore_rules() throws IOException { Files.write(projectDirPath.resolve(GITIGNORE_FILENAME), List.of("app/", "!frontend/app/"), java.nio.file.StandardOpenOption.CREATE); var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(projectDirPath); assertThat(sonarLintGitIgnore.isIgnored(Path.of("frontend/app/should_not_be_ignored.js"))).isFalse(); assertThat(sonarLintGitIgnore.isIgnored(Path.of("should_be_ignored.js"))).isFalse(); assertThat(sonarLintGitIgnore.isIgnored(Path.of("app/should_be_ignored.js"))).isTrue(); } @Test void createSonarLintGitIgnore_works_for_bare_repos_too() { var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(bareRepoPath); assertThat(sonarLintGitIgnore.isFileIgnored(Path.of("file.txt"))).isFalse(); assertThat(sonarLintGitIgnore.isFileIgnored(Path.of("file.tmp"))).isTrue(); assertThat(sonarLintGitIgnore.isFileIgnored(Path.of("file.log"))).isTrue(); } @Test void nonAsciiCharacterFileName() { var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(bareRepoPath); assertThat(sonarLintGitIgnore.isIgnored(Path.of("Sönar.txt"))).isFalse(); assertThat(sonarLintGitIgnore.isIgnored(Path.of("Sönar.log"))).isTrue(); } @Test void should_not_read_git_ignore_on_bare_repo_with_no_commit(@TempDir Path bareRepoNoCommitPath) throws GitAPIException { try (var ignored = Git.init().setBare(true).setDirectory(bareRepoNoCommitPath.toFile()).call()) { var sonarLintGitIgnore = GitService.createSonarLintGitIgnore(bareRepoNoCommitPath); assertThat(sonarLintGitIgnore.isIgnored(Path.of("Sonar.txt"))).isFalse(); assertThat(sonarLintGitIgnore.isIgnored(Path.of("Sonar.log"))).isFalse(); } } @Test void git_blame_works_for_bare_repos_too() { var sonarLintBlameResult = blameWithGitFilesBlameLibrary(bareRepoPath, Stream.of("fileA", "fileB").map(Path::of).collect(Collectors.toSet()), null); assertThat(sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1, 2))).isPresent(); assertThat(sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(3))).isEmpty(); assertThat(sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileB"), List.of(1, 2))).isPresent(); assertThat(sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileB"), List.of(3))).isEmpty(); } @Test void should_return_empty_blame_result_if_no_commits_in_repo() throws IOException, GitAPIException { FileUtils.deleteDirectory(projectDirPath.resolve(".git").toFile()); try (var ignored = Git.init().setDirectory(projectDirPath.toFile()).call()) { createFile(projectDirPath, "fileA", "line1", "line2", "line3"); var filePath = Path.of("fileA"); var sonarLintBlameResult = blameWithGitFilesBlameLibrary(projectDirPath, Set.of(filePath), null); assertThat(sonarLintBlameResult.getLatestChangeDateForLinesInFile(Path.of("fileA"), List.of(1))).isEmpty(); } } @Test void it_should_only_return_files_under_baseDir() throws IOException, GitAPIException { // Create files in root and in a subfolder var rootFile = "rootFile.txt"; var subDir = projectDirPath.resolve("subdir"); Files.createDirectories(subDir); var subFile = subDir.resolve("subFile.txt"); createFile(projectDirPath, rootFile, "root"); createFile(subDir, "subFile.txt", "sub"); // Add and commit both files git.add().addFilepattern(rootFile).call(); git.add().addFilepattern("subdir/subFile.txt").call(); commit(git, rootFile); commit(git, "subdir/subFile.txt"); // Modify both files (so they appear as changed) modifyFile(projectDirPath.resolve(rootFile), "root", "changed"); modifyFile(subFile, "sub", "changed"); // getVCSChangedFiles for subdir should only return subFile var changedFiles = getVCSChangedFiles(subDir); assertThat(changedFiles) .contains(subFile.toUri()) .doesNotContain(projectDirPath.resolve(rootFile).toUri()); } @Test void it_should_get_remote_url() throws GitAPIException, URISyntaxException { // Set up a remote URL for the test repository var remoteUrl = "https://github.com/org/project.git"; git.remoteAdd() .setName("origin") .setUri(new URIish(remoteUrl)) .call(); var retrievedUrl = GitService.getRemoteUrl(projectDirPath); assertThat(retrievedUrl).isEqualTo(remoteUrl); } @Test void it_should_return_null_when_no_origin_remote() { var retrievedUrl = GitService.getRemoteUrl(projectDirPath); assertThat(retrievedUrl).isNull(); } @Test void it_should_return_null_for_null_base_dir() { var retrievedUrl = GitService.getRemoteUrl(null); assertThat(retrievedUrl).isNull(); } @Test void it_should_return_null_for_non_git_directory(@TempDir Path nonGitDir) { var retrievedUrl = GitService.getRemoteUrl(nonGitDir); assertThat(retrievedUrl).isNull(); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .anyMatch(s -> s.contains("Git repository not found for")); } @Test void it_should_get_remote_url_from_subdirectory() throws GitAPIException, IOException, URISyntaxException { var remoteUrl = "git@github.com:org/project.git"; git.remoteAdd() .setName("origin") .setUri(new URIish(remoteUrl)) .call(); var subDir = projectDirPath.resolve("subdirectory"); Files.createDirectories(subDir); var retrievedUrl = GitService.getRemoteUrl(subDir); assertThat(retrievedUrl).isEqualTo(remoteUrl); } @Test void it_should_return_null_when_config_access_fails() throws GitAPIException, URISyntaxException, IOException { var remoteUrl = "https://github.com/org/project.git"; git.remoteAdd() .setName("origin") .setUri(new URIish(remoteUrl)) .call(); var gitConfigFile = projectDirPath.resolve(".git").resolve("config"); Files.write(gitConfigFile, "invalid config content".getBytes()); var retrievedUrl = GitService.getRemoteUrl(projectDirPath); assertThat(retrievedUrl).isNull(); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .anyMatch(s -> s.contains("Error retrieving remote URL for")); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitLocatorTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.stream.Stream; import org.assertj.core.api.AssertionsForClassTypes; import org.eclipse.jgit.util.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.jupiter.api.condition.OS.WINDOWS; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; class NativeGitLocatorTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static NativeGitLocator underTest; @TempDir private Path projectDirPath; @BeforeEach void prepare() { underTest = spy(new NativeGitLocator()); } @AfterEach void cleanup() throws IOException { FileUtils.delete(projectDirPath.toFile(), RECURSIVE); } @Test void shouldConsiderNativeGitNotAvailableOnNull() { doReturn(Optional.empty()).when(underTest).getGitExecutable(); assertThat(underTest.getNativeGitExecutable()).isEmpty(); } @EnabledOnOs(WINDOWS) @ParameterizedTest @MethodSource("gitLocations") void should_return_first_git_location(TestData testData, Optional expectedLocation) { var location = NativeGitLocator.locateGitOnWindows(testData.whereToolResult, testData.lines()); AssertionsForClassTypes.assertThat(location).isEqualTo(expectedLocation); } private static Stream gitLocations() { return Stream.of( Arguments.of(result(0, ""), Optional.empty()), Arguments.of(result(1, "invalid location"), Optional.empty()), Arguments.of(result(0, "C:\\Program Files\\Git\\bin\\git.exe"), Optional.of("C:\\Program Files\\Git\\bin\\git.exe")), Arguments.of(result(0, "C:\\Users\\user.name\\AppData\\Local\\Programs\\Git\\cmd\\git.exe" + System.lineSeparator() + "C:\\Users\\user.name\\AppData\\Local\\Programs\\Git\\mingw64\\bin\\git.exe"), Optional.of("C:\\Users\\user.name\\AppData\\Local\\Programs\\Git\\cmd\\git.exe"))); } private static TestData result(int code, String output) { return new TestData(new ProcessWrapperFactory.ProcessExecutionResult(code), output); } private record TestData(ProcessWrapperFactory.ProcessExecutionResult whereToolResult, String lines) { } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.time.Period; import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.List; import java.util.Set; import java.util.TimeZone; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.commitAtDate; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createFile; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.createRepository; import static org.sonarsource.sonarlint.core.commons.testutils.GitUtils.modifyFile; class NativeGitTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir private Path projectDirPath; private Git git; @BeforeEach void prepare() throws Exception { git = createRepository(projectDirPath); } @Test void it_should_default_to_instant_now_git_blame_history_limit_if_older_than_one_year() throws IOException, GitAPIException { var nativeGitExecutable = new NativeGitLocator().getNativeGitExecutable(); assumeTrue(nativeGitExecutable.isPresent()); var underTest = nativeGitExecutable.get(); var calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.add(Calendar.YEAR, -2); var fileAStr = "fileA"; createFile(projectDirPath, fileAStr, "line1"); var yearAgo = calendar.toInstant(); // initial commit 2 years ago commitAtDate(git, yearAgo, fileAStr); var lines = new String[3]; // second commit 4 months after initial commit calendar.add(Calendar.MONTH, 4); lines[0] = "line1"; lines[1] = "line2"; var eightMonthsAgo = calendar.toInstant(); modifyFile(projectDirPath.resolve(fileAStr), lines); commitAtDate(git, eightMonthsAgo, fileAStr); // third commit 4 months after second commit calendar.add(Calendar.MONTH, 4); lines[2] = "line3"; var oneYearAndFourMonthsAgo = calendar.toInstant(); modifyFile(projectDirPath.resolve(fileAStr), lines); commitAtDate(git, oneYearAndFourMonthsAgo, fileAStr); var fileA = Path.of(fileAStr); var blameResult = underTest.blame(projectDirPath, Set.of(projectDirPath.resolve(fileA).toUri()), Instant.now()); var line1Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(1)).get(); var line2Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(2)).get(); var line3Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(3)).get(); assertThat(ChronoUnit.MINUTES.between(line1Date, oneYearAndFourMonthsAgo)).isZero(); assertThat(ChronoUnit.MINUTES.between(line2Date, oneYearAndFourMonthsAgo)).isZero(); assertThat(ChronoUnit.MINUTES.between(line3Date, oneYearAndFourMonthsAgo)).isZero(); } @Test void it_should_blame_file_since_effective_blame_period() throws IOException, GitAPIException { var nativeGitExecutable = new NativeGitLocator().getNativeGitExecutable(); assumeTrue(nativeGitExecutable.isPresent()); var underTest = nativeGitExecutable.get(); var calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.add(Calendar.MONTH, -18); var fileAStr = "fileA"; createFile(projectDirPath, fileAStr, "line1"); var yearAgo = calendar.toInstant(); // initial commit 1 year ago commitAtDate(git, yearAgo, fileAStr); var lines = new String[3]; // second commit 4 months after initial commit calendar.add(Calendar.MONTH, 4); lines[0] = "line1"; lines[1] = "line2"; var eightMonthsAgo = calendar.toInstant(); modifyFile(projectDirPath.resolve(fileAStr), lines); commitAtDate(git, eightMonthsAgo, fileAStr); // third commit 4 months after second commit calendar.add(Calendar.MONTH, 4); lines[2] = "line3"; var fourMonthsAgo = calendar.toInstant(); modifyFile(projectDirPath.resolve(fileAStr), lines); commitAtDate(git, fourMonthsAgo, fileAStr); var fileA = Path.of(fileAStr); var blameResult = underTest.blame(projectDirPath, Set.of(projectDirPath.resolve(fileA).toUri()), Instant.now().minus(Period.ofDays(180))); var line1Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(1)).get(); var line2Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(2)).get(); var line3Date = blameResult.getLatestChangeDateForLinesInFile(fileA, List.of(3)).get(); // provided blame time limit is 180 days, but effective period will be 1 year // line 1 was committed 1 year ago but should have commit date of the first commit made earlier than blame time window - 8 months ago assertThat(ChronoUnit.MINUTES.between(line2Date, line1Date)).isZero(); // line 2 was committed 8 months ago, it's outside the blame time window, but it's a first commit outside the range, so it has real commit date assertThat(ChronoUnit.MINUTES.between(line2Date, eightMonthsAgo)).isZero(); // line 3 was committed 4 months ago, it's inside the blame time window, so it has real commit date assertThat(ChronoUnit.MINUTES.between(line3Date, fourMonthsAgo)).isZero(); } @Test void it_should_not_blame_file_on_git_command_error() { var nativeGitExecutable = new NativeGitLocator().getNativeGitExecutable(); assumeTrue(nativeGitExecutable.isPresent()); var underTest = nativeGitExecutable.get(); var fileAStr = "fileA"; var fileA = projectDirPath.resolve(fileAStr); underTest.blame(projectDirPath, Set.of(fileA.toUri()), Instant.now()); assertThat(logTester.logs()).contains("fatal: no such path 'fileA' in HEAD", "Command failed with code: 128"); } @Test void it_should_successfully_parse_windows_like_output() { var version = NativeGit.parseGitVersionOutput(List.of("git version 2.49.0.windows.1")); assertThat(version).contains(Version.create("2.49")); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/util/git/ProcessWrapperFactoryTests.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.util.git; import java.io.IOException; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; class ProcessWrapperFactoryTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_execute_git(@TempDir Path baseDir) { assumeTrue(new NativeGitLocator().getNativeGitExecutable().isPresent()); var lines = new StringBuilder(); var result = new ProcessWrapperFactory().create(baseDir, lines::append, "git", "--version").execute(); assertThat(result.exitCode()).isZero(); assertThat(lines).contains("git version "); } @Test void it_should_return_output_for_invalid_command(@TempDir Path baseDir) { assumeTrue(new NativeGitLocator().getNativeGitExecutable().isPresent()); var processWrapper = new ProcessWrapperFactory().create(baseDir, l -> { }, "git", "-version"); var result = processWrapper.execute(); assertThat(result.exitCode()).isEqualTo(129); assertThat(logTester.logs()).contains("unknown option: -version"); } @Test void it_should_gracefully_return_output_for_interrupted_exception(@TempDir Path baseDir) throws InterruptedException, IOException { assumeTrue(new NativeGitLocator().getNativeGitExecutable().isPresent()); var lines = new StringBuilder(); var processWrapper = new ProcessWrapperFactory().create(baseDir, lines::append, "git", "--version"); var spy = spy(processWrapper); doThrow(InterruptedException.class).when(spy).runProcessAndGetOutput(any()); var result = spy.execute(); assertThat(result.exitCode()).isEqualTo(-1); assertThat(lines).contains(""); } @Test void it_should_gracefully_return_output_for_exception(@TempDir Path baseDir) throws InterruptedException, IOException { assumeTrue(new NativeGitLocator().getNativeGitExecutable().isPresent()); var lines = new StringBuilder(); var processWrapper = new ProcessWrapperFactory().create(baseDir, lines::append, "git", "--version"); var spy = spy(processWrapper); doThrow(RuntimeException.class).when(spy).runProcessAndGetOutput(any()); var result = spy.execute(); assertThat(result.exitCode()).isEqualTo(-1); assertThat(lines).contains(""); } @Test void it_should_gracefully_return_output_when_not_able_to_create_process(@TempDir Path baseDir) throws IOException { var lines = new StringBuilder(); var processWrapper = new ProcessWrapperFactory().create(baseDir, lines::append, "git", "--version"); var spy = spy(processWrapper); doThrow(IOException.class).when(spy).createProcess(); var result = spy.execute(); assertThat(result.exitCode()).isEqualTo(-2); assertThat(lines).contains(""); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/validation/InvalidFieldsTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.validation; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class InvalidFieldsTest { public static final String[] EXPECTED = {"name1", "name2", "name3"}; @Test void should_have_no_invalid_fields_initially() { InvalidFields tested = new InvalidFields(); assertThat(tested.hasInvalidFields()).isFalse(); } @Test void should_have_invalid_fields_after_adding_one() { InvalidFields tested = new InvalidFields(); tested.add("name1"); assertThat(tested.hasInvalidFields()).isTrue(); } @Test void should_include_all_added_fields() { InvalidFields tested = new InvalidFields(); tested.add("name1"); tested.add("name2"); tested.add("name3"); String[] names = tested.getNames(); assertThat(names).containsExactly(EXPECTED); } } ================================================ FILE: backend/commons/src/test/java/org/sonarsource/sonarlint/core/commons/validation/RegexpValidatorTest.java ================================================ /* * SonarLint Core - Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons.validation; import java.util.Map; import java.util.regex.PatternSyntaxException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class RegexpValidatorTest { public static final String TEST_REGEXP = "[0-9]+"; @Test void should_throw_exception_on_invalid_regexp() { assertThatThrownBy(() -> new RegexpValidator("[4-[8)")) .isInstanceOf(PatternSyntaxException.class); } @Test void should_return_empty_invalid_fields() { RegexpValidator validator = new RegexpValidator(TEST_REGEXP); InvalidFields invalidFields = validator.validateAll(Map.of( "field1", "12345", "field2", "455668", "field3", "0" )); assertThat(invalidFields.hasInvalidFields()).isFalse(); assertThat(invalidFields.getNames()).isEmpty(); } @Test void should_return_one_invalid_field() { RegexpValidator validator = new RegexpValidator(TEST_REGEXP); InvalidFields invalidFields = validator.validateAll(Map.of( "field1", "12345", "field2", "-455668", "field3", "0" )); assertThat(invalidFields.hasInvalidFields()).isTrue(); assertThat(invalidFields.getNames()) .containsExactlyInAnyOrder("field2"); } @Test void should_return_all_invalid_fields() { RegexpValidator validator = new RegexpValidator(TEST_REGEXP); InvalidFields invalidFields = validator.validateAll(Map.of( "field1", "sqrt(12345)", "field2", "-455668", "field3", "^0^" )); assertThat(invalidFields.hasInvalidFields()).isTrue(); assertThat(invalidFields.getNames()) .containsExactlyInAnyOrder("field1", "field2", "field3"); } } ================================================ FILE: backend/commons/src/test/resources/logback.xml ================================================ ================================================ FILE: backend/core/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-core SonarLint Core - Implementation Common library used by some SonarLint flavors com.google.code.findbugs jsr305 provided org.slf4j slf4j-api jakarta.inject jakarta.inject-api javax.annotation javax.annotation-api jakarta.annotation jakarta.annotation-api ${project.groupId} sonarlint-commons ${project.version} ${project.groupId} sonarlint-telemetry ${project.version} ${project.groupId} sonarlint-plugin-commons ${project.version} ${project.groupId} sonarlint-rule-extractor ${project.version} ${project.groupId} sonarlint-analysis-engine ${project.version} ${project.groupId} sonarlint-server-connection ${project.version} ${project.groupId} sonarlint-rpc-protocol ${project.version} ${project.groupId} sonarlint-http ${project.version} commons-io commons-io org.apache.commons commons-lang3 org.apache.commons commons-compress org.apache.commons commons-text commons-codec commons-codec com.google.guava guava org.bouncycastle bcpg-jdk18on org.bouncycastle bcprov-jdk18on org.apache.httpcomponents.client5 httpclient5 org.slf4j slf4j-api org.apache.httpcomponents.core5 httpcore5 5.3.2 com.squareup.okhttp3 okhttp test com.squareup.okhttp3 mockwebserver3 test org.wiremock wiremock-jetty12 test org.junit.jupiter junit-jupiter-params test org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test org.awaitility awaitility test uk.org.webcompere system-stubs-jupiter test ch.qos.logback logback-classic test src/main/resources true ondemand/plugins.properties src/main/resources false ondemand/plugins.properties com.googlecode.maven-download-plugin download-maven-plugin download-cfamily-signature generate-resources wget https://binaries.sonarsource.com/CommercialDistribution/sonar-cfamily-plugin/sonar-cfamily-plugin-${cfamily.version}.jar.asc ${project.build.outputDirectory}/ondemand sonar-cpp-plugin.jar.asc false download-csharp-signature generate-resources wget https://binaries.sonarsource.com/Distribution/sonar-csharp-plugin/sonar-csharp-plugin-${csharp.version}.jar.asc ${project.build.outputDirectory}/ondemand sonar-cs-plugin.jar.asc false download-omnisharp-mono-signature generate-resources wget https://binaries.sonarsource.com/OmniSharp-Roslyn/${omnisharp.version}/omnisharp-mono.tar.gz.asc ${project.build.outputDirectory}/ondemand omnisharp-mono.tar.gz.asc false download-omnisharp-net472-signature generate-resources wget https://binaries.sonarsource.com/OmniSharp-Roslyn/${omnisharp.version}/omnisharp-net472.tar.gz.asc ${project.build.outputDirectory}/ondemand omnisharp-net472.tar.gz.asc false download-omnisharp-net6-signature generate-resources wget https://binaries.sonarsource.com/OmniSharp-Roslyn/${omnisharp.version}/omnisharp-net6.0.tar.gz.asc ${project.build.outputDirectory}/ondemand omnisharp-net6.0.tar.gz.asc false conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/BindingCandidatesFinder.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import jakarta.inject.Inject; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class BindingCandidatesFinder { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configRepository; private final BindingClueProvider bindingClueProvider; private final SonarProjectsCache sonarProjectsCache; @Inject public BindingCandidatesFinder(ConfigurationRepository configRepository, BindingClueProvider bindingClueProvider, SonarProjectsCache sonarProjectsCache) { this.configRepository = configRepository; this.bindingClueProvider = bindingClueProvider; this.sonarProjectsCache = sonarProjectsCache; } public Set findConfigScopesToBind(String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { var configScopeCandidates = configRepository.getAllBindableUnboundScopes(); if (configScopeCandidates.isEmpty()) { return Set.of(); } var goodConfigScopeCandidates = new HashSet(); for (var scope : configScopeCandidates) { checkIfScopeIsGoodCandidateForBinding(scope, connectionId, projectKey, cancelMonitor) .ifPresent(goodConfigScopeCandidates::add); } // if both a parent and a child configuration scope are candidates, preference should be given to the higher scope in the hierarchy // we prefer to bind at the broadest possible scope return filterOutLeafCandidates(goodConfigScopeCandidates); } private Optional checkIfScopeIsGoodCandidateForBinding( ConfigurationScope scope, String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { cancelMonitor.checkCanceled(); var cluesAndConnections = bindingClueProvider.collectBindingCluesWithConnections(scope.id(), Set.of(connectionId), cancelMonitor); var cluesWithMatchingProjectKey = cluesAndConnections.stream() .filter(c -> projectKey.equals(c.getBindingClue().getSonarProjectKey())) .toList(); if (!cluesWithMatchingProjectKey.isEmpty()) { var isFromSharedConfiguration = cluesWithMatchingProjectKey.stream().anyMatch( c -> c.getBindingClue().getOrigin() == BindingSuggestionOrigin.SHARED_CONFIGURATION); if (isFromSharedConfiguration) { return Optional.of(new ConfigurationScopeSharedContext(scope, BindingSuggestionOrigin.SHARED_CONFIGURATION)); } var isFromPropertiesFile = cluesWithMatchingProjectKey.stream().anyMatch( c -> c.getBindingClue().getOrigin() == BindingSuggestionOrigin.PROPERTIES_FILE); if (isFromPropertiesFile) { return Optional.of(new ConfigurationScopeSharedContext(scope, BindingSuggestionOrigin.PROPERTIES_FILE)); } var firstOrigin = cluesWithMatchingProjectKey.get(0).getBindingClue().getOrigin(); return Optional.of(new ConfigurationScopeSharedContext(scope, firstOrigin)); } var configScopeName = scope.name(); if (isNotBlank(configScopeName) && isConfigScopeNameCloseEnoughToSonarProject(configScopeName, connectionId, projectKey, cancelMonitor)) { return Optional.of(new ConfigurationScopeSharedContext(scope, BindingSuggestionOrigin.PROJECT_NAME)); } return Optional.empty(); } private static Set filterOutLeafCandidates(Set candidates) { var candidateIds = candidates.stream().map(ConfigurationScopeSharedContext::getConfigurationScope).map(ConfigurationScope::id).collect(Collectors.toSet()); return candidates.stream().filter(bindableConfig -> { var scope = bindableConfig.getConfigurationScope(); var parentId = scope.parentId(); return parentId == null || !candidateIds.contains(parentId); }).collect(Collectors.toSet()); } private boolean isConfigScopeNameCloseEnoughToSonarProject(String configScopeName, String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { // FIXME: it looks a bit overkill to create a TextSearchIndex with just one element, apparently just to verify that the configScopeName is a good enough match for the SonarProject var sonarProjectOpt = sonarProjectsCache.getSonarProject(connectionId, projectKey, cancelMonitor); if (sonarProjectOpt.isEmpty()) { LOG.debug("Unable to find SonarProject with key '{}' on connection '{}' in the cache", projectKey, connectionId); return false; } TextSearchIndex index = new TextSearchIndex<>(); var p = sonarProjectOpt.get(); index.index(p, p.key() + " " + p.name()); var searchResult = index.search(configScopeName); return !searchResult.isEmpty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/BindingClueProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Properties; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.connection.AbstractConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion; import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.sonarsource.sonarlint.core.commons.log.SonarLintLogger.singlePlural; public class BindingClueProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONAR_SCANNER_CONFIG_FILENAME = "sonar-project.properties"; private static final String AUTOSCAN_CONFIG_FILENAME = ".sonarcloud.properties"; static final Set ALL_BINDING_CLUE_FILENAMES = Set.of(SONAR_SCANNER_CONFIG_FILENAME, AUTOSCAN_CONFIG_FILENAME); private final ConnectionConfigurationRepository connectionRepository; private final ClientFileSystemService clientFs; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; public BindingClueProvider(ConnectionConfigurationRepository connectionRepository, ClientFileSystemService clientFs, SonarCloudActiveEnvironment sonarCloudActiveEnvironment) { this.connectionRepository = connectionRepository; this.clientFs = clientFs; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; } public List collectBindingCluesWithConnections(String configScopeId, Set connectionIds, SonarLintCancelMonitor cancelMonitor) { var bindingClues = collectBindingClues(configScopeId, cancelMonitor); return matchConnections(bindingClues, connectionIds); } private List matchConnections(List bindingClues, Set eligibleConnectionIds) { LOG.debug("Match connections..."); List cluesAndConnections = new ArrayList<>(); for (var bindingClue : bindingClues) { var connectionsIds = matchConnections(bindingClue, eligibleConnectionIds); if (!connectionsIds.isEmpty()) { cluesAndConnections.add(new BindingClueWithConnections(bindingClue, connectionsIds)); } } LOG.debug("{} {} having at least one matching connection", cluesAndConnections.size(), singlePlural(cluesAndConnections.size(), "clue")); return cluesAndConnections; } public static class BindingClueWithConnections { private final BindingClue bindingClue; private final Set connectionIds; BindingClueWithConnections(BindingClue bindingClue, Set connectionIds) { this.bindingClue = bindingClue; this.connectionIds = connectionIds; } public BindingClue getBindingClue() { return bindingClue; } public Set getConnectionIds() { return connectionIds; } } public List collectBindingClues(String checkedConfigScopeId, SonarLintCancelMonitor cancelMonitor) { var sonarlintConfigurationFiles = clientFs.findSonarlintConfigurationFilesByScope(checkedConfigScopeId); if (!sonarlintConfigurationFiles.isEmpty()) { var collectedClues = collectFromFiles(sonarlintConfigurationFiles, cancelMonitor); if (!collectedClues.isEmpty()) { LOG.debug("Found {} binding {} from SonarLint configuration files", collectedClues.size(), singlePlural(collectedClues.size(), "clue")); return collectedClues; } } var bindingCluesFiles = clientFs.findFilesByNamesInScope(checkedConfigScopeId, List.copyOf(ALL_BINDING_CLUE_FILENAMES)); if (!bindingCluesFiles.isEmpty()) { var collectedClues = collectFromFiles(bindingCluesFiles, cancelMonitor); if (!collectedClues.isEmpty()) { LOG.debug("Found {} binding {}", collectedClues.size(), singlePlural(collectedClues.size(), "clue")); return collectedClues; } } LOG.debug("No binding clues were found"); return Collections.emptyList(); } private List collectFromFiles(List files, SonarLintCancelMonitor cancelMonitor) { var bindingClues = new ArrayList(); for (var foundFile : files) { cancelMonitor.checkCanceled(); var scannerProps = extractConnectionProperties(foundFile); if (scannerProps == null || hasBlankValues(scannerProps)) { continue; } var bindingClue = computeBindingClue(foundFile.getFileName(), scannerProps); if (bindingClue != null) { if (foundFile.isSonarlintConfigurationFile() && !(bindingClue instanceof UnknownBindingClue)) { LOG.debug("Found a SonarLint configuration file with a clue"); } bindingClues.add(bindingClue); } } return bindingClues; } private static boolean hasBlankValues(BindingProperties scannerProps) { var serverUrl = scannerProps.serverUrl; var projectKey = scannerProps.projectKey; var organization = scannerProps.organization; if (serverUrl == null) { return isEmptyScConfig(projectKey, organization); } return isEmptySqConfig(projectKey, serverUrl); } private static boolean isEmptySqConfig(@Nullable String projectKey, @Nullable String serverUrl) { return isBlank(projectKey) && isBlank(serverUrl); } private static boolean isEmptyScConfig(@Nullable String projectKey, @Nullable String organization) { return isBlank(projectKey) && isBlank(organization); } private Set matchConnections(BindingClue bindingClue, Set eligibleConnectionIds) { if (bindingClue instanceof SonarQubeBindingClue sonarQubeBindingClue) { var serverUrl = sonarQubeBindingClue.serverUrl; return eligibleConnectionIds.stream().map(connectionRepository::getConnectionById) .filter(SonarQubeConnectionConfiguration.class::isInstance) .map(SonarQubeConnectionConfiguration.class::cast) .filter(c -> c.isSameServerUrl(serverUrl)) .map(AbstractConnectionConfiguration::getConnectionId) .collect(toSet()); } if (bindingClue instanceof SonarCloudBindingClue sonarCloudBindingClue) { var organization = sonarCloudBindingClue.organization; return eligibleConnectionIds.stream().map(connectionRepository::getConnectionById) .filter(SonarCloudConnectionConfiguration.class::isInstance) .map(SonarCloudConnectionConfiguration.class::cast) .filter(c -> organization == null || Objects.equals(organization, c.getOrganization())) .map(AbstractConnectionConfiguration::getConnectionId) .collect(toSet()); } return eligibleConnectionIds; } @CheckForNull private static BindingProperties extractSonarLintConfiguration(ClientFile sonarLintConfigurationFile) { try { var configuration = new Gson().fromJson(sonarLintConfigurationFile.getContent(), JsonObject.class); var projectKey = configuration.get("projectKey"); var organization = configuration.get("sonarCloudOrganization"); var serverUrl = configuration.get("sonarQubeUri"); var region = configuration.get("region"); // Checking for PascalCase due to VS backward compatibility if (projectKey == null || ((organization == null) == (serverUrl == null))) { projectKey = configuration.get("ProjectKey"); organization = configuration.get("SonarCloudOrganization"); serverUrl = configuration.get("SonarQubeUri"); } return new BindingProperties(projectKey != null ? projectKey.getAsString() : null, organization != null ? organization.getAsString() : null, serverUrl != null ? serverUrl.getAsString() : null, region != null ? region.getAsString() : null, BindingSuggestionOrigin.SHARED_CONFIGURATION); } catch (Exception e) { LOG.warn("Unable to parse candidate connected mode configuration file", e); return null; } } @CheckForNull private static BindingProperties extractConnectionProperties(ClientFile matchedFile) { LOG.debug("Extracting scanner properties from {}", matchedFile); if (matchedFile.isSonarlintConfigurationFile()) { return extractSonarLintConfiguration(matchedFile); } else { var properties = new Properties(); try { properties.load(new StringReader(matchedFile.getContent())); } catch (Exception e) { LOG.error("Unable to parse content of file '{}'", matchedFile, e); return null; } return new BindingProperties(getAndTrim(properties, "sonar.projectKey"), getAndTrim(properties, "sonar.organization"), getAndTrim(properties, "sonar.host.url"), getAndTrim(properties, "sonar.region"), BindingSuggestionOrigin.PROPERTIES_FILE); } } @CheckForNull private static String getAndTrim(Properties properties, String key) { return trimToNull(properties.getProperty(key)); } private static class BindingProperties { private final String projectKey; private final String organization; private final String serverUrl; private final BindingSuggestionOrigin origin; private final SonarCloudRegion region; private BindingProperties(@Nullable String projectKey, @Nullable String organization, @Nullable String serverUrl, @Nullable String region, BindingSuggestionOrigin origin) { this.projectKey = projectKey; this.organization = organization; this.serverUrl = serverUrl; this.origin = origin; SonarCloudRegion configuredRegion; try { configuredRegion = region != null ? SonarCloudRegion.valueOf(region.toUpperCase(Locale.ENGLISH)) : SonarCloudRegion.EU; } catch (IllegalArgumentException e) { LOG.warn("Cannot accept '{}' as a valid SonarQube Cloud region while reading shared Connected Mode settings. Falling back to EU region", region); configuredRegion = SonarCloudRegion.EU; } this.region = configuredRegion; } } @CheckForNull private BindingClue computeBindingClue(String filename, BindingProperties scannerProps) { if (AUTOSCAN_CONFIG_FILENAME.equals(filename)) { return new SonarCloudBindingClue(scannerProps.projectKey, scannerProps.organization, scannerProps.region, scannerProps.origin); } if (scannerProps.organization != null) { return new SonarCloudBindingClue(scannerProps.projectKey, scannerProps.organization, scannerProps.region, scannerProps.origin); } if (scannerProps.serverUrl != null) { if (sonarCloudActiveEnvironment.isSonarQubeCloud(scannerProps.serverUrl)) { return new SonarCloudBindingClue(scannerProps.projectKey, null, scannerProps.region, scannerProps.origin); } else { return new SonarQubeBindingClue(scannerProps.projectKey, scannerProps.serverUrl, scannerProps.origin); } } if (scannerProps.projectKey != null) { return new UnknownBindingClue(scannerProps.projectKey, scannerProps.origin); } return null; } public interface BindingClue { @CheckForNull String getSonarProjectKey(); BindingSuggestionOrigin getOrigin(); } public static class UnknownBindingClue implements BindingClue { private final String sonarProjectKey; BindingSuggestionOrigin origin; UnknownBindingClue(String sonarProjectKey, BindingSuggestionOrigin origin) { this.sonarProjectKey = sonarProjectKey; this.origin = origin; } @Override public String getSonarProjectKey() { return sonarProjectKey; } @Override public BindingSuggestionOrigin getOrigin() { return origin; } } public static class SonarQubeBindingClue implements BindingClue { private final String sonarProjectKey; private final String serverUrl; BindingSuggestionOrigin origin; SonarQubeBindingClue(@Nullable String sonarProjectKey, String serverUrl, BindingSuggestionOrigin origin) { this.sonarProjectKey = sonarProjectKey; this.serverUrl = serverUrl; this.origin = origin; } @Override public String getSonarProjectKey() { return sonarProjectKey; } @Override public BindingSuggestionOrigin getOrigin() { return origin; } public String getServerUrl() { return serverUrl; } } public static class SonarCloudBindingClue implements BindingClue { private final String sonarProjectKey; private final String organization; private final SonarCloudRegion region; BindingSuggestionOrigin origin; SonarCloudBindingClue(@Nullable String sonarProjectKey, @Nullable String organization, @Nullable SonarCloudRegion region, BindingSuggestionOrigin origin) { this.sonarProjectKey = sonarProjectKey; this.organization = organization; this.region = region != null ? region : SonarCloudRegion.EU; this.origin = origin; } @Override public String getSonarProjectKey() { return sonarProjectKey; } @Override public BindingSuggestionOrigin getOrigin() { return origin; } public String getOrganization() { return organization; } public SonarCloudRegion getRegion() { return region; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/BindingSuggestionProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.commons.util.git.GitService; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.SuggestBindingParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.springframework.context.event.EventListener; import static java.lang.String.join; import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.sonarsource.sonarlint.core.commons.log.SonarLintLogger.singlePlural; public class BindingSuggestionProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configRepository; private final ConnectionConfigurationRepository connectionRepository; private final SonarLintRpcClient client; private final BindingClueProvider bindingClueProvider; private final SonarProjectsCache sonarProjectsCache; private final ExecutorServiceShutdownWatchable executorService; private final AtomicBoolean enabled = new AtomicBoolean(true); private final SonarQubeClientManager sonarQubeClientManager; private final ClientFileSystemService clientFs; private final TelemetryService telemetryService; @Inject public BindingSuggestionProvider(ConfigurationRepository configRepository, ConnectionConfigurationRepository connectionRepository, SonarLintRpcClient client, BindingClueProvider bindingClueProvider, SonarProjectsCache sonarProjectsCache, SonarQubeClientManager sonarQubeClientManager, ClientFileSystemService clientFs, TelemetryService telemetryService) { this.configRepository = configRepository; this.connectionRepository = connectionRepository; this.client = client; this.bindingClueProvider = bindingClueProvider; this.sonarProjectsCache = sonarProjectsCache; this.sonarQubeClientManager = sonarQubeClientManager; this.clientFs = clientFs; this.telemetryService = telemetryService; this.executorService = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadExecutor("Binding Suggestion Provider")); } @EventListener public void bindingConfigChanged(BindingConfigChangedEvent event) { // Check if binding suggestion was switched on if (!event.newConfig().bindingSuggestionDisabled() && event.previousConfig().bindingSuggestionDisabled()) { suggestBindingForGivenScopesAndAllConnections(Set.of(event.configScopeId())); } } public void suggestBindingForGivenScopesAndAllConnections(Set configScopeIdsToSuggest) { if (!configScopeIdsToSuggest.isEmpty()) { var allConnectionIds = connectionRepository.getConnectionsById().keySet(); if (allConnectionIds.isEmpty()) { LOG.debug("No connections configured, skipping binding suggestions."); return; } LOG.debug("Binding suggestion computation queued for config scopes '{}'...", join(",", configScopeIdsToSuggest)); queueBindingSuggestionComputation(configScopeIdsToSuggest, allConnectionIds); } } @EventListener public void connectionAdded(ConnectionConfigurationAddedEvent event) { // Double check if added connection has not been removed in the meantime var addedConnectionId = event.addedConnectionId(); var allConfigScopeIds = configRepository.getConfigScopeIds(); if (connectionRepository.getConnectionById(addedConnectionId) != null && !allConfigScopeIds.isEmpty()) { LOG.debug("Binding suggestions computation queued for connection '{}'...", addedConnectionId); var candidateConnectionIds = Set.of(addedConnectionId); queueBindingSuggestionComputation(allConfigScopeIds, candidateConnectionIds); } } public Map> getBindingSuggestions(String configScopeId, String connectionId, SonarLintCancelMonitor cancelMonitor) { return computeBindingSuggestions(Set.of(configScopeId), Set.of(connectionId), cancelMonitor); } private void queueBindingSuggestionComputation(Set configScopeIds, Set candidateConnectionIds) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); executorService.execute(() -> { if (enabled.get()) { computeAndNotifyBindingSuggestions(configScopeIds, candidateConnectionIds, cancelMonitor); } else { LOG.debug("Skipping binding suggestion computation as it is disabled"); } }); } private void computeAndNotifyBindingSuggestions(Set configScopeIds, Set candidateConnectionIds, SonarLintCancelMonitor cancelMonitor) { Map> suggestionsByConfigScope = computeBindingSuggestions(configScopeIds, candidateConnectionIds, cancelMonitor); if (!suggestionsByConfigScope.isEmpty()) { client.suggestBinding(new SuggestBindingParams(suggestionsByConfigScope)); } } private Map> computeBindingSuggestions(Set configScopeIds, Set candidateConnectionIds, SonarLintCancelMonitor cancelMonitor) { var eligibleConfigScopesForBindingSuggestion = new HashSet(); for (var configScopeId : configScopeIds) { cancelMonitor.checkCanceled(); if (isScopeEligibleForBindingSuggestion(configScopeId)) { eligibleConfigScopesForBindingSuggestion.add(configScopeId); } } if (eligibleConfigScopesForBindingSuggestion.isEmpty()) { return emptyMap(); } var suggestionsByConfigScope = new HashMap>(); for (var configScopeId : eligibleConfigScopesForBindingSuggestion) { cancelMonitor.checkCanceled(); var scopeSuggestions = suggestBindingForEligibleScope(configScopeId, candidateConnectionIds, cancelMonitor); LOG.debug("Found {} {} for configuration scope '{}'", scopeSuggestions.size(), singlePlural(scopeSuggestions.size(), "suggestion"), configScopeId); if (!scopeSuggestions.isEmpty()) { suggestionsByConfigScope.put(configScopeId, scopeSuggestions); } } return suggestionsByConfigScope; } private List suggestBindingForEligibleScope(String checkedConfigScopeId, Set candidateConnectionIds, SonarLintCancelMonitor cancelMonitor) { var cluesAndConnections = bindingClueProvider.collectBindingCluesWithConnections(checkedConfigScopeId, candidateConnectionIds, cancelMonitor); List suggestions = new ArrayList<>(); var cluesWithProjectKey = cluesAndConnections.stream().filter(c -> c.getBindingClue().getSonarProjectKey() != null).toList(); for (var bindingClueWithConnections : cluesWithProjectKey) { var sonarProjectKey = requireNonNull(bindingClueWithConnections.getBindingClue().getSonarProjectKey()); for (var connectionId : bindingClueWithConnections.getConnectionIds()) { sonarProjectsCache .getSonarProject(connectionId, sonarProjectKey, cancelMonitor) .ifPresent(serverProject -> suggestions.add(new BindingSuggestionDto(connectionId, sonarProjectKey, serverProject.name(), bindingClueWithConnections.getBindingClue().getOrigin()))); } } if (suggestions.isEmpty()) { var configScopeName = Optional.ofNullable(configRepository.getConfigurationScope(checkedConfigScopeId)).map(ConfigurationScope::name).orElse(null); if (isNotBlank(configScopeName)) { var cluesWithoutProjectKey = cluesAndConnections.stream().filter(c -> c.getBindingClue().getSonarProjectKey() == null).toList(); for (var bindingClueWithConnections : cluesWithoutProjectKey) { searchGoodMatchInConnections(suggestions, configScopeName, bindingClueWithConnections.getConnectionIds(), cancelMonitor); } if (cluesWithoutProjectKey.isEmpty()) { searchGoodMatchInConnections(suggestions, configScopeName, candidateConnectionIds, cancelMonitor); } } } if (suggestions.isEmpty()) { searchByRemoteUrlInConnections(suggestions, checkedConfigScopeId, candidateConnectionIds, cancelMonitor); if (!suggestions.isEmpty()) { telemetryService.suggestedRemoteBinding(); } } return suggestions; } private void searchGoodMatchInConnections(List suggestions, String configScopeName, Set connectionIdsToSearch, SonarLintCancelMonitor cancelMonitor) { for (var connectionId : connectionIdsToSearch) { searchGoodMatchInConnection(suggestions, configScopeName, connectionId, cancelMonitor); } } private void searchByRemoteUrlInConnections(List suggestions, String configScopeId, Set connectionIds, SonarLintCancelMonitor cancelMonitor) { var remoteUrl = GitService.getRemoteUrl(clientFs.getBaseDir(configScopeId)); if (remoteUrl == null) { LOG.debug("No remote URL found for configuration scope '{}", configScopeId); return; } for (var connectionId : connectionIds) { try { var suggestion = sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(connectionId, api -> getBindingSuggestionByRemoteUrl(cancelMonitor, connectionId, api, remoteUrl)); suggestion.ifPresent(suggestions::add); } catch (Exception e) { LOG.debug("Failed to get binding suggestion by remote URL for connection '{}': {}", connectionId, e.getMessage()); } } } @NotNull private static Optional getBindingSuggestionByRemoteUrl(SonarLintCancelMonitor cancelMonitor, String connectionId, ServerApi api, String remoteUrl) { if (api.isSonarCloud()) { var sqcResponse = api.projectBindings().getSQCProjectBindings(remoteUrl, cancelMonitor); if (sqcResponse != null) { var searchResponse = api.component().searchProjects(sqcResponse.projectId(), cancelMonitor); if (searchResponse != null) { return Optional.of(new BindingSuggestionDto(connectionId, searchResponse.projectKey(), searchResponse.projectName(), BindingSuggestionOrigin.REMOTE_URL)); } } } else { var sqsResponse = api.projectBindings().getSQSProjectBindings(remoteUrl, cancelMonitor); if (sqsResponse != null) { var serverProject = api.component().getProject(sqsResponse.projectKey(), cancelMonitor); if (serverProject.isPresent()) { return Optional.of(new BindingSuggestionDto(connectionId, sqsResponse.projectKey(), serverProject.get().name(), BindingSuggestionOrigin.REMOTE_URL)); } } } return Optional.empty(); } private void searchGoodMatchInConnection(List suggestions, String configScopeName, String connectionId, SonarLintCancelMonitor cancelMonitor) { LOG.debug("Attempt to find a good match for '{}' on connection '{}'...", configScopeName, connectionId); var index = sonarProjectsCache.getTextSearchIndex(connectionId, cancelMonitor); var searchResult = index.search(configScopeName); if (!searchResult.isEmpty()) { Double bestScore = Double.MIN_VALUE; for (var serverProjectScoreEntry : searchResult.entrySet()) { if (serverProjectScoreEntry.getValue() < bestScore) { break; } bestScore = serverProjectScoreEntry.getValue(); suggestions.add(new BindingSuggestionDto(connectionId, serverProjectScoreEntry.getKey().key(), serverProjectScoreEntry.getKey().name(), BindingSuggestionOrigin.PROJECT_NAME)); } LOG.debug("Best score = {}", String.format(Locale.ENGLISH, "%,.2f", bestScore)); } } private boolean isScopeEligibleForBindingSuggestion(String configScopeId) { var configScope = configRepository.getConfigurationScope(configScopeId); var bindingConfiguration = configRepository.getBindingConfiguration(configScopeId); if (configScope == null || bindingConfiguration == null) { // Race condition LOG.debug("Configuration scope '{}' is gone.", configScopeId); return false; } if (!configScope.bindable()) { LOG.debug("Configuration scope '{}' is not bindable.", configScopeId); return false; } if (isValidBinding(bindingConfiguration)) { LOG.debug("Configuration scope '{}' is already bound.", configScopeId); return false; } if (bindingConfiguration.bindingSuggestionDisabled()) { LOG.debug("Configuration scope '{}' has binding suggestions disabled.", configScopeId); return false; } return true; } private boolean isValidBinding(BindingConfiguration bindingConfiguration) { return bindingConfiguration.ifBound((connectionId, projectKey) -> connectionRepository.getConnectionById(connectionId) != null) .orElse(false); } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop binding suggestions executor service in a timely manner"); } } public void disable() { this.enabled.set(false); } public void enable() { this.enabled.set(true); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ConfigurationScopeSharedContext.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; public class ConfigurationScopeSharedContext { private final ConfigurationScope configurationScope; private final BindingSuggestionOrigin origin; ConfigurationScopeSharedContext(ConfigurationScope configurationScope, BindingSuggestionOrigin origin) { this.configurationScope = configurationScope; this.origin = origin; } public ConfigurationScope getConfigurationScope() { return configurationScope; } public BindingSuggestionOrigin getOrigin() { return origin; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ConfigurationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.HashSet; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScopeWithBinding; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; public class ConfigurationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ApplicationEventPublisher applicationEventPublisher; private final ConfigurationRepository repository; public ConfigurationService(ApplicationEventPublisher applicationEventPublisher, ConfigurationRepository repository) { this.applicationEventPublisher = applicationEventPublisher; this.repository = repository; } public void didAddConfigurationScopes(List addedScopes) { var addedIds = new HashSet(); for (var addedDto : addedScopes) { var configScopeInReferential = adapt(addedDto); var bindingDto = addedDto.getBinding(); var bindingConfigInReferential = adapt(bindingDto); var previous = repository.addOrReplace(configScopeInReferential, bindingConfigInReferential); if (previous != null) { LOG.error("Duplicate configuration scope registered: {}", addedDto.getId()); } else { LOG.debug("Added configuration scope '{}'", configScopeInReferential.id()); addedIds.add(new ConfigurationScopeWithBinding(configScopeInReferential, bindingConfigInReferential)); } } if (!addedIds.isEmpty()) { applicationEventPublisher.publishEvent(new ConfigurationScopesAddedWithBindingEvent(addedIds)); } } private static BindingConfiguration adapt(@Nullable BindingConfigurationDto dto) { if (dto == null) { return BindingConfiguration.noBinding(); } return new BindingConfiguration(dto.getConnectionId(), dto.getSonarProjectKey(), dto.isBindingSuggestionDisabled()); } private static ConfigurationScope adapt(ConfigurationScopeDto dto) { return new ConfigurationScope(dto.getId(), dto.getParentId(), dto.isBindable(), dto.getName()); } public void didRemoveConfigurationScope(String removedId) { var removed = repository.remove(removedId); if (removed == null) { LOG.debug("Attempt to remove configuration scope '{}' that was not registered", removedId); } else { LOG.debug("Removed configuration scope '{}'", removedId); applicationEventPublisher.publishEvent(new ConfigurationScopeRemovedEvent(removed.scope(), removed.bindingConfiguration())); } } public void didUpdateBinding(String configScopeId, BindingConfigurationDto updatedBinding) { LOG.debug("Did update binding for configuration scope '{}', new binding: '{}'", configScopeId, updatedBinding); var boundEvent = bind(configScopeId, updatedBinding); if (boundEvent != null) { applicationEventPublisher.publishEvent(boundEvent); } } @EventListener public void connectionRemoved(ConnectionConfigurationRemovedEvent event) { var bindingConfigurationByConfigScope = repository.removeBindingForConnection(event.removedConnectionId()); bindingConfigurationByConfigScope.forEach((configScope, bindingConfiguration) -> applicationEventPublisher.publishEvent(new BindingConfigChangedEvent(configScope, bindingConfiguration, BindingConfiguration.noBinding(bindingConfiguration.bindingSuggestionDisabled())))); } @CheckForNull private BindingConfigChangedEvent bind(String configurationScopeId, BindingConfigurationDto bindingConfiguration) { var previousBindingConfig = repository.getBindingConfiguration(configurationScopeId); if (previousBindingConfig == null) { LOG.error("Attempt to update binding in configuration scope '{}' that was not registered", configurationScopeId); return null; } var newBindingConfig = adapt(bindingConfiguration); repository.updateBinding(configurationScopeId, newBindingConfig); return createChangedEventIfNeeded(configurationScopeId, previousBindingConfig, newBindingConfig); } @CheckForNull private static BindingConfigChangedEvent createChangedEventIfNeeded(String configScopeId, BindingConfiguration previousBindingConfig, BindingConfiguration newBindingConfig) { if (!previousBindingConfig.equals(newBindingConfig)) { return new BindingConfigChangedEvent(configScopeId, previousBindingConfig, newBindingConfig); } return null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import jakarta.inject.Inject; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.validation.InvalidFields; import org.sonarsource.sonarlint.core.commons.validation.RegexpValidator; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.event.ConnectionCredentialsChangedEvent; import org.sonarsource.sonarlint.core.repository.connection.AbstractConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarQubeConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.SonarProjectDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.validate.ValidateConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; import org.sonarsource.sonarlint.core.serverconnection.ServerVersionAndStatusChecker; import org.springframework.context.ApplicationEventPublisher; import static java.util.stream.Collectors.toMap; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.UNAUTHORIZED; public class ConnectionService { private static final SonarLintLogger LOG = SonarLintLogger.get(); /** * Validator for strings containing small letters, numbers and "-" symbols. */ private static final RegexpValidator REGEXP_VALIDATOR = new RegexpValidator("[a-z0-9\\-]+"); private final ApplicationEventPublisher applicationEventPublisher; private final ConnectionConfigurationRepository repository; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; private final SonarQubeClientManager sonarQubeClientManager; private final TokenGeneratorHelper tokenGeneratorHelper; @Inject public ConnectionService(ApplicationEventPublisher applicationEventPublisher, ConnectionConfigurationRepository repository, InitializeParams params, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, TokenGeneratorHelper tokenGeneratorHelper, SonarQubeClientManager sonarQubeClientManager) { this(applicationEventPublisher, repository, params.getSonarQubeConnections(), params.getSonarCloudConnections(), sonarCloudActiveEnvironment, sonarQubeClientManager, tokenGeneratorHelper); } ConnectionService(ApplicationEventPublisher applicationEventPublisher, ConnectionConfigurationRepository repository, @Nullable List initSonarQubeConnections, @Nullable List initSonarCloudConnections, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, SonarQubeClientManager sonarQubeClientManager, TokenGeneratorHelper tokenGeneratorHelper) { this.applicationEventPublisher = applicationEventPublisher; this.repository = repository; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; this.sonarQubeClientManager = sonarQubeClientManager; this.tokenGeneratorHelper = tokenGeneratorHelper; if (initSonarQubeConnections != null) { initSonarQubeConnections.forEach(c -> repository.addOrReplace(adapt(c))); } if (initSonarCloudConnections != null) { initSonarCloudConnections.forEach(c -> repository.addOrReplace(adapt(c))); } } private static SonarQubeConnectionConfiguration adapt(SonarQubeConnectionConfigurationDto sqDto) { return new SonarQubeConnectionConfiguration(sqDto.getConnectionId(), sqDto.getServerUrl(), sqDto.getDisableNotifications()); } private SonarCloudConnectionConfiguration adapt(SonarCloudConnectionConfigurationDto scDto) { var region = SonarCloudRegion.valueOf(scDto.getRegion().toString()); return new SonarCloudConnectionConfiguration(sonarCloudActiveEnvironment.getUri(region), sonarCloudActiveEnvironment.getApiUri(region), scDto.getConnectionId(), scDto.getOrganization(), region, scDto.isDisableNotifications()); } private static void putAndLogIfDuplicateId(Map map, AbstractConnectionConfiguration config) { if (map.put(config.getConnectionId(), config) != null) { LOG.error("Duplicate connection registered: {}", config.getConnectionId()); } } public void didUpdateConnections(List sonarQubeConnections, List sonarCloudConnections) { var newConnectionsById = new HashMap(); sonarQubeConnections.forEach(config -> putAndLogIfDuplicateId(newConnectionsById, adapt(config))); sonarCloudConnections.forEach(config -> putAndLogIfDuplicateId(newConnectionsById, adapt(config))); var previousConnectionsById = repository.getConnectionsById(); var updatedConnections = newConnectionsById.entrySet().stream() .filter(e -> previousConnectionsById.containsKey(e.getKey())) .filter(e -> !previousConnectionsById.get(e.getKey()).equals(e.getValue())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); var addedConnections = newConnectionsById.entrySet().stream() .filter(e -> !previousConnectionsById.containsKey(e.getKey())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); var removedConnectionIds = new HashSet<>(previousConnectionsById.keySet()); removedConnectionIds.removeAll(newConnectionsById.keySet()); updatedConnections.values().forEach(this::updateConnection); addedConnections.values().forEach(this::addConnection); removedConnectionIds.forEach(this::removeConnection); } public void didChangeCredentials(String connectionId) { applicationEventPublisher.publishEvent(new ConnectionCredentialsChangedEvent(connectionId)); } private void addConnection(AbstractConnectionConfiguration connectionConfiguration) { repository.addOrReplace(connectionConfiguration); LOG.debug("Connection '{}' added", connectionConfiguration.getConnectionId()); applicationEventPublisher.publishEvent(new ConnectionConfigurationAddedEvent(connectionConfiguration.getConnectionId(), connectionConfiguration.getKind())); } private void removeConnection(String removedConnectionId) { var removed = repository.remove(removedConnectionId); if (removed == null) { LOG.debug("Attempt to remove connection '{}' that was not registered. Possibly a race condition?", removedConnectionId); } else { LOG.debug("Connection '{}' removed", removedConnectionId); applicationEventPublisher.publishEvent(new ConnectionConfigurationRemovedEvent(removedConnectionId)); } } private void updateConnection(AbstractConnectionConfiguration connectionConfiguration) { var connectionId = connectionConfiguration.getConnectionId(); var previous = repository.addOrReplace(connectionConfiguration); if (previous == null) { LOG.debug("Attempt to update connection '{}' that was not registered. Possibly a race condition?", connectionId); applicationEventPublisher.publishEvent(new ConnectionConfigurationAddedEvent(connectionConfiguration.getConnectionId(), connectionConfiguration.getKind())); } else { LOG.debug("Connection '{}' updated", previous.getConnectionId()); applicationEventPublisher.publishEvent(new ConnectionConfigurationUpdatedEvent(connectionConfiguration.getConnectionId())); } } public ValidateConnectionResponse validateConnection(Either transientConnection, SonarLintCancelMonitor cancelMonitor) { try { var serverApi = sonarQubeClientManager.getForTransientConnection(transientConnection); var serverChecker = new ServerVersionAndStatusChecker(serverApi); serverChecker.checkVersionAndStatus(cancelMonitor); var validateCredentials = serverApi.authentication().validate(cancelMonitor); if (validateCredentials.success() && transientConnection.isRight()) { var organizationKey = transientConnection.getRight().getOrganization(); if (organizationKey != null) { var organization = serverApi.organization().searchOrganization(organizationKey, cancelMonitor); if (organization.isEmpty()) { return new ValidateConnectionResponse(false, "No organizations found for key: " + organizationKey); } } } return new ValidateConnectionResponse(validateCredentials.success(), validateCredentials.message()); } catch (Exception e) { LOG.error("Error validating connection", e); return new ValidateConnectionResponse(false, e.getMessage()); } } public HelpGenerateUserTokenResponse helpGenerateUserToken(String serverUrl, @Nullable HelpGenerateUserTokenParams.Utm utm, SonarLintCancelMonitor cancelMonitor) { if (utm != null) { var invalidFields = validateUtm(utm); if (invalidFields.hasInvalidFields()) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "UTM parameters should match regular expression: [a-z0-9\\-]+", invalidFields.getNames())); } } return tokenGeneratorHelper.helpGenerateUserToken(serverUrl, utm, cancelMonitor); } private static InvalidFields validateUtm(HelpGenerateUserTokenParams.Utm utm) { return REGEXP_VALIDATOR.validateAll(Map.of( "utm_medium", utm.getMedium(), "utm_source", utm.getSource(), "utm_content", utm.getContent(), "utm_term", utm.getTerm())); } public List getAllProjects(Either transientConnection, SonarLintCancelMonitor cancelMonitor) { var serverApi = sonarQubeClientManager.getForTransientConnection(transientConnection); try { return serverApi.component().getAllProjects(cancelMonitor) .stream().map(serverProject -> new SonarProjectDto(serverProject.key(), serverProject.name())) .toList(); } catch (UnauthorizedException e) { throw new ResponseErrorException(new ResponseError(UNAUTHORIZED, "The authorization has failed. Please check your credentials.", null)); } } public Map getProjectNamesByKey(Either transientConnection, List projectKeys, SonarLintCancelMonitor cancelMonitor) { var serverApi = sonarQubeClientManager.getForTransientConnection(transientConnection); var projectNamesByKey = new HashMap(); projectKeys.forEach(key -> { var projectName = serverApi.component().getProject(key, cancelMonitor).map(ServerProject::name).orElse(null); projectNamesByKey.put(key, projectName); }); return projectNamesByKey; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ConnectionSuggestionProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import jakarta.inject.Inject; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.Strings; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.fs.FileSystemUpdatedEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarQubeConnectionSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SuggestConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.springframework.context.event.EventListener; import static org.sonarsource.sonarlint.core.BindingClueProvider.ALL_BINDING_CLUE_FILENAMES; public class ConnectionSuggestionProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configRepository; private final ConnectionConfigurationRepository connectionRepository; private final SonarLintRpcClient client; private final BindingClueProvider bindingClueProvider; private final ExecutorServiceShutdownWatchable executorService; private final BindingSuggestionProvider bindingSuggestionProvider; private final ClientFileSystemService clientFs; @Inject public ConnectionSuggestionProvider(ConfigurationRepository configRepository, ConnectionConfigurationRepository connectionRepository, SonarLintRpcClient client, BindingClueProvider bindingClueProvider, BindingSuggestionProvider bindingSuggestionProvider, ClientFileSystemService clientFs) { this.configRepository = configRepository; this.connectionRepository = connectionRepository; this.client = client; this.bindingClueProvider = bindingClueProvider; this.executorService = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadExecutor("Connection Suggestion Provider")); this.bindingSuggestionProvider = bindingSuggestionProvider; this.clientFs = clientFs; } @EventListener public void filesystemUpdated(FileSystemUpdatedEvent event) { var listConfigScopeIds = event.getAddedOrUpdated().stream() .filter(f -> ALL_BINDING_CLUE_FILENAMES.contains(f.getFileName()) || f.isSonarlintConfigurationFile()) .map(ClientFile::getConfigScopeId) .collect(Collectors.toSet()); queueConnectionSuggestion(listConfigScopeIds); } @EventListener public void configurationScopesAdded(ConfigurationScopesAddedWithBindingEvent event) { var listConfigScopeIds = event.getConfigScopeIds().stream() .map(clientFs::getFiles) .flatMap(List::stream) .filter(f -> ALL_BINDING_CLUE_FILENAMES.contains(f.getFileName()) || f.isSonarlintConfigurationFile()) .map(ClientFile::getConfigScopeId) .collect(Collectors.toSet()); if (!listConfigScopeIds.isEmpty()) { queueConnectionSuggestion(listConfigScopeIds); } else { bindingSuggestionProvider.suggestBindingForGivenScopesAndAllConnections(event.getConfigScopeIds()); } } private void queueConnectionSuggestion(Set listConfigScopeIds) { if (!listConfigScopeIds.isEmpty()) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); executorService.execute(() -> suggestConnectionAndBindingForGivenScopes(listConfigScopeIds, cancelMonitor)); } } private void suggestConnectionAndBindingForGivenScopes(Set configScopeIds, SonarLintCancelMonitor cancelMonitor) { var connectionAndBindingSuggestions = computeConnectionAndBindingSuggestions(configScopeIds, cancelMonitor); suggestConnectionToClientIfAny(connectionAndBindingSuggestions.connectionSuggestionsByConfigScopeIds()); computeBindingSuggestionIfAny(connectionAndBindingSuggestions.bindingSuggestionsForConfigScopeIds()); } private @NotNull ConnectionAndBindingSuggestions computeConnectionAndBindingSuggestions(Set configScopeIds, SonarLintCancelMonitor cancelMonitor) { LOG.debug("Computing connection suggestions"); var connectionSuggestionsByConfigScopeIds = new HashMap>(); var bindingSuggestionsForConfigScopeIds = new HashSet(); for (var configScopeId : configScopeIds) { var effectiveBinding = configRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isPresent()) { LOG.debug("A binding already exists, skipping the connection suggestion"); continue; } var bindingClues = bindingClueProvider.collectBindingClues(configScopeId, cancelMonitor); for (var bindingClue : bindingClues) { var projectKey = bindingClue.getSonarProjectKey(); if (projectKey != null) { handleBindingClue(bindingClue).ifPresentOrElse(clue -> clue.map( serverUrl -> connectionSuggestionsByConfigScopeIds.computeIfAbsent(configScopeId, s -> new ArrayList<>()) .add(new ConnectionSuggestionDto(new SonarQubeConnectionSuggestionDto(serverUrl, projectKey), bindingClue.getOrigin())), organization -> connectionSuggestionsByConfigScopeIds.computeIfAbsent(configScopeId, s -> new ArrayList<>()) .add(new ConnectionSuggestionDto(new SonarCloudConnectionSuggestionDto(organization, projectKey, ((BindingClueProvider.SonarCloudBindingClue) bindingClue).getRegion()), bindingClue.getOrigin()))), () -> bindingSuggestionsForConfigScopeIds.add(configScopeId)); } } } return new ConnectionAndBindingSuggestions(connectionSuggestionsByConfigScopeIds, bindingSuggestionsForConfigScopeIds); } private Optional> handleBindingClue(BindingClueProvider.BindingClue bindingClue) { switch (bindingClue) { case BindingClueProvider.SonarCloudBindingClue sonarCloudBindingClue -> { LOG.debug("Found a SonarCloud binding clue"); var organization = sonarCloudBindingClue.getOrganization(); var connection = connectionRepository.findByOrganization(organization); if (connection.isEmpty()) { return Optional.of(Either.forRight(organization)); } } case BindingClueProvider.SonarQubeBindingClue sonarQubeBindingClue -> { LOG.debug("Found a SonarQube binding clue"); var serverUrl = sonarQubeBindingClue.getServerUrl(); var connection = connectionRepository.findByUrl(serverUrl); if (connection.isEmpty()) { return Optional.of(Either.forLeft(Strings.CS.removeEnd(serverUrl, "/"))); } } default -> LOG.debug("Found an invalid binding clue for connection suggestion"); } return Optional.empty(); } private void suggestConnectionToClientIfAny(Map> connectionSuggestionsByConfigScopeIds) { if (!connectionSuggestionsByConfigScopeIds.isEmpty()) { var foundSuggestionsCount = connectionSuggestionsByConfigScopeIds.size(); LOG.debug("Found {} connection {}", foundSuggestionsCount, SonarLintLogger.singlePlural(foundSuggestionsCount, "suggestion")); client.suggestConnection(new SuggestConnectionParams(connectionSuggestionsByConfigScopeIds)); } } private void computeBindingSuggestionIfAny(Set bindingSuggestionsForConfigScopeIds) { if (!bindingSuggestionsForConfigScopeIds.isEmpty()) { LOG.debug("Found binding suggestion(s) for %s configuration scope IDs", bindingSuggestionsForConfigScopeIds.size()); bindingSuggestionProvider.suggestBindingForGivenScopesAndAllConnections(bindingSuggestionsForConfigScopeIds); } } public List getConnectionSuggestions(String configScopeId, SonarLintCancelMonitor cancelMonitor) { var connectionAndBindingSuggestions = computeConnectionAndBindingSuggestions(Set.of(configScopeId), cancelMonitor); return connectionAndBindingSuggestions.connectionSuggestionsByConfigScopeIds.containsKey(configScopeId) ? connectionAndBindingSuggestions.connectionSuggestionsByConfigScopeIds.get(configScopeId) : List.of(); } private record ConnectionAndBindingSuggestions( Map> connectionSuggestionsByConfigScopeIds, Set bindingSuggestionsForConfigScopeIds ) {} } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/DtoMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.MQRModeDetails; import org.sonarsource.sonarlint.core.rpc.protocol.common.StandardModeDetails; import org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.tracking.TextRangeUtils.toTextRangeDto; public class DtoMapper { private DtoMapper() { // util } public static RaisedIssueDto toRaisedIssueDto(TrackedIssue issue, NewCodeDefinition newCodeDefinition, boolean isMQRMode, boolean isAiCodeFixable) { return new RaisedIssueDto(issue.getId(), issue.getServerKey(), issue.getRuleKey(), issue.getMessage(), isMQRMode ? Either.forRight(new MQRModeDetails(RuleDetailsAdapter.adapt(issue.getCleanCodeAttribute()), RuleDetailsAdapter.toDto(issue.getImpacts()))) : Either.forLeft(new StandardModeDetails(RuleDetailsAdapter.adapt(issue.getSeverity()), RuleDetailsAdapter.adapt(issue.getType()))), requireNonNull(issue.getIntroductionDate()), newCodeDefinition.isOnNewCode(issue.getIntroductionDate()), issue.isResolved(), toTextRangeDto(issue.getTextRangeWithHash()), issue.getFlows().stream().map(RuleDetailsAdapter::adapt).toList(), issue.getQuickFixes().stream().map(RuleDetailsAdapter::adapt).toList(), issue.getRuleDescriptionContextKey(), isAiCodeFixable, issue.getResolutionStatus()); } public static RaisedHotspotDto toRaisedHotspotDto(TrackedIssue issue, NewCodeDefinition newCodeDefinition, boolean isMQRMode) { var status = issue.getHotspotStatus(); status = status != null ? status : HotspotStatus.TO_REVIEW; var vp = RuleDetailsAdapter.adapt(issue.getVulnerabilityProbability()); if (vp == null) { // this should not normally happen because all hotspots supposed to have the vulnerability probability set throw new IllegalStateException("Vulnerability probability should be set for security hotspots"); } return new RaisedHotspotDto(issue.getId(), issue.getServerKey(), issue.getRuleKey(), issue.getMessage(), isMQRMode && !issue.getImpacts().isEmpty() ? Either.forRight(new MQRModeDetails(RuleDetailsAdapter.adapt(issue.getCleanCodeAttribute()), RuleDetailsAdapter.toDto(issue.getImpacts()))) : Either.forLeft(new StandardModeDetails(RuleDetailsAdapter.adapt(issue.getSeverity()), RuleDetailsAdapter.adapt(issue.getType()))), requireNonNull(issue.getIntroductionDate()), newCodeDefinition.isOnNewCode(issue.getIntroductionDate()), issue.isResolved(), toTextRangeDto(issue.getTextRangeWithHash()), issue.getFlows().stream().map(RuleDetailsAdapter::adapt).toList(), issue.getQuickFixes().stream().map(RuleDetailsAdapter::adapt).toList(), issue.getRuleDescriptionContextKey(), vp, status); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/MCPServerConfigurationProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.SonarLintException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static java.lang.String.format; public class MCPServerConfigurationProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONARCLOUD_MCP_CONFIG = """ { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "SONARQUBE_TOKEN", "-e", "SONARQUBE_ORG", "-e", "SONARQUBE_CLOUD_URL", "-e", "SONARQUBE_IDE_PORT", "mcp/sonarqube" ], "env": { "SONARQUBE_ORG": "%s", "SONARQUBE_CLOUD_URL": "%s", "SONARQUBE_TOKEN": "%s", "SONARQUBE_IDE_PORT": "%s" } } """; private static final String SONARQUBE_MCP_CONFIG = """ { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "SONARQUBE_TOKEN", "-e", "SONARQUBE_URL", "-e", "SONARQUBE_IDE_PORT", "mcp/sonarqube" ], "env": { "SONARQUBE_URL": "%s", "SONARQUBE_TOKEN": "%s", "SONARQUBE_IDE_PORT": "%s" } } """; private final ConnectionConfigurationRepository connectionRepository; private final TelemetryService telemetryService; private final EmbeddedServer embeddedServer; public MCPServerConfigurationProvider(ConnectionConfigurationRepository connectionRepository, TelemetryService telemetryService, EmbeddedServer embeddedServer) { this.connectionRepository = connectionRepository; this.telemetryService = telemetryService; this.embeddedServer = embeddedServer; } public String getMCPServerConfigurationJSON(String connectionId, String token) { var connection = connectionRepository.getConnectionById(connectionId); if (connection != null) { telemetryService.mcpServerConfigurationRequested(); if (connection.getKind() == ConnectionKind.SONARCLOUD) { var sonarCloudConnection = (SonarCloudConnectionConfiguration) connection; var organization = sonarCloudConnection.getOrganization(); var url = connection.getUrl(); return format(SONARCLOUD_MCP_CONFIG, organization, url, token, embeddedServer.getPort()); } else { var url = connection.getUrl(); return format(SONARQUBE_MCP_CONFIG, url, token, embeddedServer.getPort()); } } else { LOG.warn("Request for generating MCP server settings JSON failed; Connection not found for '{}'", connectionId); throw new SonarLintException(format("Connection not found for '%s'; Cannot generate MCP server settings JSON", connectionId)); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/OrganizationsCache.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.OrganizationDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.commons.log.SonarLintLogger.singlePlural; /** * Cache user organizations index for a certain amount of time. */ public class OrganizationsCache { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarQubeClientManager sonarQubeClientManager; private final Cache, TextSearchIndex> textSearchIndexCacheByCredentials = CacheBuilder.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .build(); public OrganizationsCache(SonarQubeClientManager sonarQubeClientManager) { this.sonarQubeClientManager = sonarQubeClientManager; } public List fuzzySearchOrganizations(TransientSonarCloudConnectionDto transientSonarCloudConnection, String searchText, SonarLintCancelMonitor cancelMonitor) { return getTextSearchIndex(transientSonarCloudConnection, cancelMonitor).search(searchText) .entrySet() .stream() .sorted(Comparator.comparing(Map.Entry::getValue).reversed() .thenComparing(e -> e.getKey().getName(), String.CASE_INSENSITIVE_ORDER)) .limit(10) .map(Map.Entry::getKey) .toList(); } public TextSearchIndex getTextSearchIndex(TransientSonarCloudConnectionDto transientSonarCloudConnection, SonarLintCancelMonitor cancelMonitor) { try { return textSearchIndexCacheByCredentials.get(transientSonarCloudConnection.getCredentials(), () -> { LOG.debug("Load user organizations..."); List orgs; try { orgs = sonarQubeClientManager.getForTransientConnection(Either.forRight(transientSonarCloudConnection)) .organization() .listUserOrganizations(cancelMonitor).stream().map(o -> new OrganizationDto(o.getKey(), o.getName(), o.getDescription())).toList(); } catch (Exception e) { LOG.error("Error while querying SonarCloud organizations", e); return new TextSearchIndex<>(); } if (orgs.isEmpty()) { LOG.debug("No organizations found"); return new TextSearchIndex<>(); } else { LOG.debug("Creating index for {} {}", orgs.size(), singlePlural(orgs.size(), "organization")); var index = new TextSearchIndex(); orgs.forEach(org -> index.index(org, org.getKey() + " " + org.getName())); return index; } }); } catch (ExecutionException e) { throw new IllegalStateException(e.getCause()); } } public List listUserOrganizations(TransientSonarCloudConnectionDto transientSonarCloudConnection, SonarLintCancelMonitor cancelMonitor) { textSearchIndexCacheByCredentials.invalidate(transientSonarCloudConnection.getCredentials()); return getTextSearchIndex(transientSonarCloudConnection, cancelMonitor).getAll(); } @CheckForNull public OrganizationDto getOrganization(TransientSonarCloudConnectionDto transientSonarCloudConnection, SonarLintCancelMonitor cancelMonitor) { return sonarQubeClientManager.getForTransientConnection(Either.forRight(transientSonarCloudConnection)) .organization().searchOrganization(requireNonNull(transientSonarCloudConnection.getOrganization()), cancelMonitor) .map(o -> new OrganizationDto(o.getKey(), o.getName(), o.getDescription())).orElse(null); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ServerFileExclusions.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import org.apache.commons.lang3.ArrayUtils; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.config.Configuration; import org.sonar.api.scan.filesystem.FileExclusions; import org.sonarsource.sonarlint.core.analysis.container.analysis.SonarLintPathPattern; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class ServerFileExclusions { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final FileExclusions exclusionSettings; private SonarLintPathPattern[] mainInclusions; private SonarLintPathPattern[] mainExclusions; private SonarLintPathPattern[] testInclusions; private SonarLintPathPattern[] testExclusions; public ServerFileExclusions(Configuration configuration) { this.exclusionSettings = new FileExclusions(configuration); } public void prepare() { mainInclusions = prepareMainInclusions(); mainExclusions = prepareMainExclusions(); testInclusions = prepareTestInclusions(); testExclusions = prepareTestExclusions(); log("Server included sources: ", mainInclusions); log("Server excluded sources: ", mainExclusions); log("Server included tests: ", testInclusions); log("Server excluded tests: ", testExclusions); } private static void log(String title, SonarLintPathPattern[] patterns) { if (patterns.length > 0) { LOG.debug(title); for (SonarLintPathPattern pattern : patterns) { LOG.debug(" {}", pattern); } } } public boolean accept(String relativePath, InputFile.Type type) { SonarLintPathPattern[] inclusionPatterns; SonarLintPathPattern[] exclusionPatterns; switch (type) { case MAIN -> { inclusionPatterns = mainInclusions; exclusionPatterns = mainExclusions; } case TEST -> { inclusionPatterns = testInclusions; exclusionPatterns = testExclusions; } default -> throw new IllegalArgumentException("Unknown file type: " + type); } if (inclusionPatterns.length > 0) { var matchInclusion = false; for (SonarLintPathPattern pattern : inclusionPatterns) { matchInclusion |= pattern.match(relativePath); } if (!matchInclusion) { return false; } } for (SonarLintPathPattern pattern : exclusionPatterns) { if (pattern.match(relativePath)) { return false; } } return true; } SonarLintPathPattern[] prepareMainInclusions() { if (exclusionSettings.sourceInclusions().length > 0) { // User defined params return SonarLintPathPattern.create(exclusionSettings.sourceInclusions()); } return new SonarLintPathPattern[0]; } SonarLintPathPattern[] prepareTestInclusions() { return SonarLintPathPattern.create(computeTestInclusions()); } private String[] computeTestInclusions() { if (exclusionSettings.testInclusions().length > 0) { // User defined params return exclusionSettings.testInclusions(); } return ArrayUtils.EMPTY_STRING_ARRAY; } SonarLintPathPattern[] prepareMainExclusions() { var patterns = ArrayUtils.addAll( exclusionSettings.sourceExclusions(), computeTestInclusions()); return SonarLintPathPattern.create(patterns); } SonarLintPathPattern[] prepareTestExclusions() { return SonarLintPathPattern.create(exclusionSettings.testExclusions()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SharedConnectedModeSettingsProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.Objects; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.SonarLintException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static java.lang.String.format; public class SharedConnectedModeSettingsProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONARCLOUD_CONNECTED_MODE_CONFIG = """ { "sonarCloudOrganization": "%s", "projectKey": "%s", "region": "%s" }"""; private static final String SONARQUBE_CONNECTED_MODE_CONFIG = """ { "sonarQubeUri": "%s", "projectKey": "%s" }"""; private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionRepository; private final TelemetryService telemetryService; public SharedConnectedModeSettingsProvider(ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionRepository, TelemetryService telemetryService) { this.configurationRepository = configurationRepository; this.connectionRepository = connectionRepository; this.telemetryService = telemetryService; } public String getSharedConnectedModeConfigFileContents(String configScopeId) { var bindingConfiguration = configurationRepository.getBindingConfiguration(configScopeId); if (bindingConfiguration != null && bindingConfiguration.isBound()) { var projectKey = bindingConfiguration.sonarProjectKey(); var connectionId = bindingConfiguration.connectionId(); var connection = Objects.requireNonNull(connectionRepository.getConnectionById(Objects.requireNonNull(connectionId))); telemetryService.exportedConnectedMode(); if (connection.getKind() == ConnectionKind.SONARCLOUD) { var organization = ((SonarCloudConnectionConfiguration) connection).getOrganization(); var region = ((SonarCloudConnectionConfiguration) connection).getRegion(); return format(SONARCLOUD_CONNECTED_MODE_CONFIG, organization, projectKey, region); } else { return format(SONARQUBE_CONNECTED_MODE_CONFIG, connection.getUrl(), projectKey); } } else { LOG.warn("Request for generating shared Connected Mode configuration file content failed; Binding not yet available for '{}'", configScopeId); throw new SonarLintException(format("Binding not found for '%s'; Cannot generate shared Connected Mode file contents", configScopeId)); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironment.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.net.URI; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.Strings; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarQubeCloudRegionDto; public class SonarCloudActiveEnvironment { private final Map alternativeRegionUris; public static SonarCloudActiveEnvironment prod() { return new SonarCloudActiveEnvironment(Map.of()); } public SonarCloudActiveEnvironment(Map alternativeRegionUris) { this.alternativeRegionUris = alternativeRegionUris.entrySet().stream() .collect(Collectors.toMap(entry -> SonarCloudRegion.valueOf(entry.getKey().name()), Map.Entry::getValue)); } public URI getUri(SonarCloudRegion region) { if (alternativeRegionUris.containsKey(region) && alternativeRegionUris.get(region).getUri() != null) { return alternativeRegionUris.get(region).getUri(); } return region.getProductionUri(); } public URI getApiUri(SonarCloudRegion region) { if (alternativeRegionUris.containsKey(region) && alternativeRegionUris.get(region).getApiUri() != null) { return alternativeRegionUris.get(region).getApiUri(); } return region.getApiProductionUri(); } public URI getWebSocketsEndpointUri(SonarCloudRegion region) { if (alternativeRegionUris.containsKey(region) && alternativeRegionUris.get(region).getWebSocketsEndpointUri() != null) { return alternativeRegionUris.get(region).getWebSocketsEndpointUri(); } return region.getWebSocketUri(); } public boolean isSonarQubeCloud(String uri) { return getRegionByUri(uri).isPresent(); } /** * Before calling this method, caller should make sure URI is SonarCloud */ public SonarCloudRegion getRegionOrThrow(String uri) { var regionOpt = getRegionByUri(uri); if (regionOpt.isPresent()) { return regionOpt.get(); } throw new IllegalArgumentException("URI should be a known SonarCloud URI"); } private Optional getRegionByUri(String uri) { var cleanedUri = Strings.CS.removeEnd(uri, "/"); for (var entry : alternativeRegionUris.entrySet()) { var regionUri = entry.getValue().getUri(); if (regionUri != null && Strings.CS.removeEnd(regionUri.toString(), "/").equals(cleanedUri)) { return Optional.of(entry.getKey()); } } for (var region : SonarCloudRegion.values()) { if (Strings.CS.removeEnd(region.getProductionUri().toString(), "/").equals(cleanedUri)) { return Optional.of(region); } } return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCloudRegion.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.net.URI; import java.util.Arrays; public enum SonarCloudRegion { EU("https://sonarcloud.io", "https://api.sonarcloud.io", "wss://events-api.sonarcloud.io/"), US("https://sonarqube.us", "https://api.sonarqube.us", "wss://events-api.sonarqube.us/"); public static final String[] CLOUD_URLS = Arrays.stream(values()) .map(SonarCloudRegion::getProductionUri) .map(Object::toString) .toArray(String[]::new); private final URI productionUri; private final URI apiProductionUri; private final URI webSocketUri; SonarCloudRegion(String productionUri, String apiProductionUri, String webSocketUri) { this.productionUri = URI.create(productionUri); this.apiProductionUri = URI.create(apiProductionUri); this.webSocketUri = URI.create(webSocketUri); } public URI getProductionUri() { return productionUri; } public URI getApiProductionUri() { return apiProductionUri; } public URI getWebSocketUri() { return webSocketUri; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarCodeContextService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.dogfood.DogfoodEnvironmentDetectionService; import org.sonarsource.sonarlint.core.commons.util.git.ProcessWrapperFactory; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetCredentialsParams; import org.springframework.context.event.EventListener; /** * Dogfooding-only integration that runs SonarCodeContext CLI on repository open in connected mode. * Commands executed (in order): init, generate-md-guidelines, merge-md, install. * Outputs are expected under the '.sonar-code-context' directory. */ public class SonarCodeContextService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONAR_CODE_CONTEXT_DIR = ".sonar-code-context"; private static final String CLI_EXECUTABLE = "sonar-code-context"; private static final String SONAR_MD_FILENAME = "SONAR.md"; private static final String CURSOR_MDC_FILENAME = "sonar-code-context.mdc"; private final ClientFileSystemService clientFileSystemService; private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final SonarProjectBranchTrackingService branchTrackingService; private final SonarLintRpcClient client; private final ProcessWrapperFactory processWrapperFactory = new ProcessWrapperFactory(); private final boolean isEnabled; private final Set initializedScopes = new HashSet<>(); private final Set mdcInstalledScopes = new HashSet<>(); public SonarCodeContextService(DogfoodEnvironmentDetectionService dogfoodEnvDetectionService, ClientFileSystemService clientFileSystemService, ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionConfigurationRepository, SonarProjectBranchTrackingService branchTrackingService, SonarLintRpcClient client, InitializeParams params) { this.clientFileSystemService = clientFileSystemService; this.configurationRepository = configurationRepository; this.connectionConfigurationRepository = connectionConfigurationRepository; this.branchTrackingService = branchTrackingService; this.client = client; this.isEnabled = dogfoodEnvDetectionService.isDogfoodEnvironment() && params.getBackendCapabilities().contains(BackendCapability.CONTEXT_GENERATION); } @EventListener public void onConfigurationScopesAdded(ConfigurationScopesAddedWithBindingEvent event) { if (!isEnabled) { return; } for (var configScopeId : event.getConfigScopeIds()) { var baseDir = clientFileSystemService.getBaseDir(configScopeId); // Only run for scopes that are directly bound (not inherited from parent) var bindingOpt = configurationRepository.getConfiguredBinding(configScopeId); if (baseDir != null && bindingOpt.isPresent()) { handleGeneration(configScopeId, baseDir, bindingOpt.get()); } else { LOG.debug("No baseDir for configuration scope '{}' - skipping SonarCodeContext CLI", configScopeId); } } } @EventListener public void onBindingChanged(BindingConfigChangedEvent event) { if (!isEnabled) { return; } var configScopeId = event.configScopeId(); var baseDir = clientFileSystemService.getBaseDir(configScopeId); var bindingOpt = configurationRepository.getConfiguredBinding(configScopeId); if (baseDir != null && bindingOpt.isPresent()) { handleGeneration(configScopeId, baseDir, bindingOpt.get()); } } private void handleGeneration(String configScopeId, Path baseDir, Binding binding) { try { var paramsOpt = prepareCliParams(binding, configScopeId); if (paramsOpt.isPresent()) { var workingDir = computeWorkingBaseDir(baseDir); if (initializedScopes.add(configScopeId)) { runInit(workingDir); } runGenerateGuidelines(workingDir, paramsOpt.get()); runMergeMd(workingDir); if (mdcInstalledScopes.add(configScopeId)) { runInstall(workingDir); } } else { LOG.debug("Missing parameters for SonarCodeContext CLI, skipping for configuration scope '{}'", configScopeId); } } catch (Exception e) { LOG.debug("[DOGFOOD] Failed to run code context CLI", e.getMessage()); } } private Optional prepareCliParams(Binding binding, String configScopeId) { var connection = connectionConfigurationRepository.getConnectionById(binding.connectionId()); if (connection == null) { return Optional.empty(); } var url = connection.getUrl(); var token = getTokenForConnection(binding.connectionId()); if (token.isEmpty()) { return Optional.empty(); } var branch = branchTrackingService.awaitEffectiveSonarProjectBranch(configScopeId).orElse(null); return Optional.of(new CliParams(url, token.get(), binding.sonarProjectKey(), branch)); } private Optional getTokenForConnection(String connectionId) { try { var creds = client.getCredentials(new GetCredentialsParams(connectionId)).join().getCredentials(); if (creds != null && creds.isLeft()) { var tokenDto = creds.getLeft(); return Optional.ofNullable(tokenDto.getToken()); } return Optional.empty(); } catch (Exception e) { LOG.debug("Unable to retrieve token for connection '{}'", connectionId, e); return Optional.empty(); } } private void runInit(Path baseDir) { var command = new ArrayList<>(List.of(resolveCliExecutable(), "init")); execute(baseDir, command); var settings = baseDir.resolve(SONAR_CODE_CONTEXT_DIR).resolve("settings.json"); if (Files.exists(settings)) { LOG.debug("Initialized SonarCodeContext settings at {}", settings); } } private void runGenerateGuidelines(Path baseDir, CliParams params) { var command = new ArrayList<>(List.of( resolveCliExecutable(), "generate-md-guidelines", "--sq-url=" + params.sqUrl, "--sq-token=" + params.sqToken, "--sq-project-key=" + params.projectKey )); if (params.sqBranch() != null) { command.add("--sq-branch=" + params.sqBranch()); } execute(baseDir, command); } private void runMergeMd(Path baseDir) { var command = new ArrayList<>(List.of(resolveCliExecutable(), "merge-md")); execute(baseDir, command); var merged = baseDir.resolve(SONAR_CODE_CONTEXT_DIR).resolve(SONAR_MD_FILENAME); if (Files.exists(merged)) { LOG.debug("Merged {} at {}", SONAR_MD_FILENAME, merged); } else { LOG.debug("{} was not generated under {}", SONAR_MD_FILENAME, baseDir.resolve(SONAR_CODE_CONTEXT_DIR)); } } private void runInstall(Path baseDir) { var command = new ArrayList<>(List.of(resolveCliExecutable(), "install", "--force", "--cursor-mdc")); execute(baseDir, command); var cursorRule = baseDir.resolve(".cursor").resolve("rules").resolve(CURSOR_MDC_FILENAME); if (Files.exists(cursorRule)) { LOG.debug("Generated {} at {}", CURSOR_MDC_FILENAME, cursorRule); } } private void execute(Path baseDir, List command) { var result = processWrapperFactory.create(baseDir, LOG::debug, command.toArray(new String[0])).execute(); if (result.exitCode() != 0) { LOG.debug("Command '{}' exited with code {} in {}", String.join(" ", command), result.exitCode(), baseDir); } } private record CliParams(String sqUrl, String sqToken, String projectKey, @Nullable String sqBranch) {} private static Path computeWorkingBaseDir(Path baseDir) { try { var current = baseDir; while (current != null) { if (Files.isDirectory(current.resolve(".git"))) { return current; } current = current.getParent(); } } catch (Exception e) { // ignore and fallback } return baseDir; } private static String resolveCliExecutable() { // Used for testing var prop = System.getProperty("sonar.code.context.executable"); if (prop != null && !prop.isBlank()) { return prop; } return CLI_EXECUTABLE; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarLintMDC.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import javax.annotation.Nullable; import org.slf4j.MDC; public class SonarLintMDC { public static final String CONFIG_SCOPE_ID_MDC_KEY = "configScopeId"; private SonarLintMDC() { // only static methods } public static void putConfigScopeId(@Nullable String configScopeId) { if (configScopeId == null) { MDC.remove(CONFIG_SCOPE_ID_MDC_KEY); } else { MDC.put(CONFIG_SCOPE_ID_MDC_KEY, configScopeId); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarProjectsCache.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.SonarProjectDto; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import org.springframework.context.event.EventListener; import static org.sonarsource.sonarlint.core.commons.log.SonarLintLogger.singlePlural; public class SonarProjectsCache { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarQubeClientManager sonarQubeClientManager; private final Cache> textSearchIndexCacheByConnectionId = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .build(); private final Cache> singleProjectsCache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .build(); public SonarProjectsCache(SonarQubeClientManager sonarQubeClientManager) { this.sonarQubeClientManager = sonarQubeClientManager; } public List fuzzySearchProjects(String connectionId, String searchText, SonarLintCancelMonitor cancelMonitor) { return getTextSearchIndex(connectionId, cancelMonitor).search(searchText) .entrySet() .stream() .sorted(Comparator.comparing(Map.Entry::getValue).reversed() .thenComparing(Comparator.comparing(e -> e.getKey().name(), String.CASE_INSENSITIVE_ORDER))) .limit(10) .map(e -> new SonarProjectDto(e.getKey().key(), e.getKey().name())) .toList(); } private static class SonarProjectKey { private final String connectionId; private final String projectKey; private SonarProjectKey(String connectionId, String projectKey) { this.connectionId = connectionId; this.projectKey = projectKey; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } var that = (SonarProjectKey) o; return connectionId.equals(that.connectionId) && projectKey.equals(that.projectKey); } @Override public int hashCode() { return Objects.hash(connectionId, projectKey); } } @EventListener public void connectionRemoved(ConnectionConfigurationRemovedEvent e) { evictAll(e.removedConnectionId()); } @EventListener public void connectionUpdated(ConnectionConfigurationUpdatedEvent e) { // If connection config was modified (url, credentials, ...) then the projects the user might be able to "see" could be different evictAll(e.updatedConnectionId()); } private void evictAll(String connectionId) { textSearchIndexCacheByConnectionId.invalidate(connectionId); // Not possible to evict only entries of the given connection, so simply evict all singleProjectsCache.invalidateAll(); } public Optional getSonarProject(String connectionId, String sonarProjectKey, SonarLintCancelMonitor cancelMonitor) { try { return singleProjectsCache.get(new SonarProjectKey(connectionId, sonarProjectKey), () -> { LOG.debug("Query project '{}' on connection '{}'...", sonarProjectKey, connectionId); try { return sonarQubeClientManager.withActiveClientAndReturn(connectionId, s -> s.component().getProject(sonarProjectKey, cancelMonitor)).orElse(Optional.empty()); } catch (Exception e) { LOG.error("Error while querying project '{}' from connection '{}'", sonarProjectKey, connectionId, e); return Optional.empty(); } }); } catch (ExecutionException e) { throw new IllegalStateException(e.getCause()); } } public TextSearchIndex getTextSearchIndex(String connectionId, SonarLintCancelMonitor cancelMonitor) { try { return textSearchIndexCacheByConnectionId.get(connectionId, () -> { LOG.debug("Load projects from connection '{}'...", connectionId); List projects; try { projects = sonarQubeClientManager.withActiveClientAndReturn(connectionId, s -> s.component().getAllProjects(cancelMonitor)) .orElse(List.of()); } catch (Exception e) { LOG.error("Error while querying projects from connection '{}'", connectionId, e); return new TextSearchIndex<>(); } if (projects.isEmpty()) { LOG.debug("No projects found for connection '{}'", connectionId); return new TextSearchIndex<>(); } else { LOG.debug("Creating index for {} {}", projects.size(), singlePlural(projects.size(), "project")); var index = new TextSearchIndex(); projects.forEach(p -> index.index(p, p.key() + " " + p.name())); return index; } }); } catch (ExecutionException e) { throw new IllegalStateException(e.getCause()); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/SonarQubeClientManager.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nullable; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.connection.SonarQubeClient; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.event.ConnectionCredentialsChangedEvent; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.http.WebSocketClient; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarQubeConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetCredentialsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverconnection.ServerVersionAndStatusChecker; import org.springframework.context.event.EventListener; public class SonarQubeClientManager { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConnectionConfigurationRepository connectionRepository; private final HttpClientProvider httpClientProvider; private final SonarLintRpcClient client; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; private final Map> clientsByConnectionId = new ConcurrentHashMap<>(); public SonarQubeClientManager(ConnectionConfigurationRepository connectionRepository, HttpClientProvider httpClientProvider, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, SonarLintRpcClient client) { this.connectionRepository = connectionRepository; this.httpClientProvider = httpClientProvider; this.client = client; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; } /** * Throws ResponseErrorException if connection with provided ID is not found in ConnectionConfigurationRepository */ public SonarQubeClient getValidClientOrThrow(String connectionId) { return clientsByConnectionId.computeIfAbsent(connectionId, this::createSonarQubeClient) .orElseThrow(() -> new ResponseErrorException(new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Connection '" + connectionId + "' is not valid", connectionId))); } public void withActiveClient(String connectionId, Consumer serverApiConsumer) { getValidClient(connectionId).ifPresent(connection -> connection.withClientApi(serverApiConsumer)); } public Optional withActiveClientAndReturn(String connectionId, Function serverApiConsumer) { return getValidClient(connectionId).map(connection -> connection.withClientApiAndReturn(serverApiConsumer)); } public Optional withActiveClientFlatMapOptionalAndReturn(String connectionId, Function> serverApiConsumer) { return getValidClient(connectionId).map(connection -> connection.withClientApiAndReturn(serverApiConsumer)).flatMap(Function.identity()); } private Optional getValidClient(String connectionId) { return clientsByConnectionId.computeIfAbsent(connectionId, this::createSonarQubeClient) .filter(connection -> isConnectionActive(connectionId, connection)); } private Optional createSonarQubeClient(String connectionId) { var connection = connectionRepository.getConnectionById(connectionId); if (connection == null) { LOG.debug("Connection '{}' is gone", connectionId); return Optional.empty(); } var credentials = getValidCredentialsFromClient(connectionId); if (credentials.isEmpty()) { client.invalidToken(new InvalidTokenParams(connectionId)); return Optional.empty(); } var endpointParams = connection.getEndpointParams(); var isBearerSupported = checkIfBearerIsSupported(endpointParams); var httpClient = credentials.get().map( tokenDto -> httpClientProvider.getHttpClientWithPreemptiveAuth(tokenDto.getToken(), isBearerSupported), userPass -> httpClientProvider.getHttpClientWithPreemptiveAuth(userPass.getUsername(), userPass.getPassword())); return Optional.of(new SonarQubeClient(connectionId, new ServerApi(endpointParams, httpClient), credentials.get(), client)); } private static boolean isConnectionActive(String connectionId, SonarQubeClient connection) { var isValid = connection.isActive(); if (!isValid) { LOG.debug("Connection '{}' is invalid", connectionId); } return isValid; } public ServerApi getForTransientConnection(Either transientConnection) { var endpointParams = transientConnection.map( sq -> new EndpointParams(sq.getServerUrl(), null, false, null), sc -> { var region = SonarCloudRegion.valueOf(sc.getRegion().toString()); return new EndpointParams(sonarCloudActiveEnvironment.getUri(region).toString(), sonarCloudActiveEnvironment.getApiUri(region).toString(), true, sc.getOrganization()); }); var httpClient = transientConnection .map(TransientSonarQubeConnectionDto::getCredentials, TransientSonarCloudConnectionDto::getCredentials) .map( tokenDto -> { var isBearerSupported = checkIfBearerIsSupported(endpointParams); return httpClientProvider.getHttpClientWithPreemptiveAuth(tokenDto.getToken(), isBearerSupported); }, userPass -> httpClientProvider.getHttpClientWithPreemptiveAuth(userPass.getUsername(), userPass.getPassword())); return new ServerApi(new ServerApiHelper(endpointParams, httpClient)); } public Optional getValidWebSocketClient(String connectionId) { return getValidClient(connectionId) .map(validClient -> { var credentials = validClient.getCredentials(); if (credentials.isRight()) { // We are normally only supporting tokens for SonarCloud connections throw new IllegalStateException("Expected token for connection " + connectionId); } return httpClientProvider.getWebSocketClient(credentials.getLeft().getToken()); }); } private boolean checkIfBearerIsSupported(EndpointParams params) { if (params.isSonarCloud()) { return true; } var cancelMonitor = new SonarLintCancelMonitor(); var serverApi = new ServerApi(params, httpClientProvider.getHttpClientWithoutAuth()); var status = serverApi.system().getStatus(cancelMonitor); var serverChecker = new ServerVersionAndStatusChecker(serverApi); return serverChecker.isSupportingBearer(status); } private Optional> getValidCredentialsFromClient(String connectionId) { var response = client.getCredentials(new GetCredentialsParams(connectionId)).join(); var credentials = response.getCredentials(); return validateCredentials(connectionId, credentials); } private static Optional> validateCredentials(String connectionId, @Nullable Either credentials) { if (credentials == null) { LOG.error("No credentials for connection " + connectionId); return Optional.empty(); } if (credentials.isLeft()) { if (isNullOrEmpty(credentials.getLeft().getToken())) { LOG.error("No token for connection " + connectionId); return Optional.empty(); } return Optional.of(credentials); } var right = credentials.getRight(); if (right == null) { LOG.error("No username/password for connection " + connectionId); return Optional.empty(); } if (isNullOrEmpty(right.getUsername())) { LOG.error("No username for connection " + connectionId); return Optional.empty(); } if (isNullOrEmpty(right.getPassword())) { LOG.error("No password for connection " + connectionId); return Optional.empty(); } return Optional.of(credentials); } private static boolean isNullOrEmpty(@Nullable String s) { return s == null || s.trim().isEmpty(); } @EventListener public void onConnectionRemoved(ConnectionConfigurationRemovedEvent event) { clientsByConnectionId.remove(event.removedConnectionId()); } @EventListener public void onConnectionUpdated(ConnectionConfigurationUpdatedEvent event) { clientsByConnectionId.remove(event.updatedConnectionId()); } @EventListener public void onCredentialsChanged(ConnectionCredentialsChangedEvent event) { clientsByConnectionId.remove(event.getConnectionId()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/TextSearchIndex.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Indexes text associated to objects, and performs full text search to find matching objects. * It is a positional index, so it supports queries consisted of multiple terms, in which case it will find partial term matches in sequence (distance = 1). * The result is sorted by score. The score of each term matches is the ratio of the term matches (1 for exact match), * and the global score is the sum of the term's scores in the object divided by the total term frequency in the object. *

* The generic type should properly implement equals and hashCode. * An object cannot be indexed twice. *

* Performance of indexing: O(N) * Performance of search: O(log N) on the number of indexed terms + O(N) on the number of results */ public class TextSearchIndex { /** * Any non-letter, non-digit symbols in a row. Unlike [\W]+ dos include underscore as many relevant strings use it as a separator. */ private static final String DEFAULT_SPLIT_PATTERN = "[^a-zA-Z0-9]+"; private final Pattern splitPattern = Pattern.compile(DEFAULT_SPLIT_PATTERN); private final TreeMap> termToObj; private final Map objToWordFrequency; public TextSearchIndex() { termToObj = new TreeMap<>(); objToWordFrequency = new HashMap<>(); } public int size() { return objToWordFrequency.size(); } public boolean isEmpty() { return objToWordFrequency.isEmpty(); } public void index(T obj, String text) { if (objToWordFrequency.containsKey(obj)) { throw new IllegalArgumentException("Already indexed"); } var terms = tokenize(text); objToWordFrequency.put(obj, terms.size()); var i = 0; for (String s : terms) { addToDictionary(s, i, obj); i++; } } /** * Search for indexed objects based on a query. Results will be sorted by score (highest first). * Score is in the interval [0,1]. * * @return A map of results reverse-sorted by value (score). Can be empty, but never null */ public Map search(String query) { var terms = tokenize(query); if (terms.isEmpty()) { return Collections.emptyMap(); } List matched; // positional search var it = terms.iterator(); matched = searchTerm(it.next()); while (it.hasNext()) { var termMatches = searchTerm(it.next()); matched = matchPositional(matched, termMatches, 1); if (matched.isEmpty()) { break; } } // convert results and calc score return prepareResult(matched); } private List matchPositional(List previousMatches, List termMatches, int maxDistance) { List matches = new LinkedList<>(); for (SearchResult e1 : previousMatches) { for (SearchResult e2 : termMatches) { if (!e1.obj.equals(e2.obj)) { continue; } var dist = e2.lastIdx - e1.lastIdx; if (dist > 0 && dist <= maxDistance) { e2.score += e1.score; matches.add(e2); } } } return matches; } private Map prepareResult(List entries) { Map objToScore = new HashMap<>(); for (SearchResult e : entries) { var score = e.score / objToWordFrequency.get(e.obj); var previousScore = objToScore.get(e.obj); if (previousScore == null || previousScore < score) { objToScore.put(e.obj, score); } } return objToScore.entrySet().stream() .sorted(Map.Entry.comparingByValue().reversed()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); } /** * Returns any term prefixed by the given text */ private List searchTerm(String termPrefix) { List entries = new LinkedList<>(); var tailMap = termToObj.tailMap(termPrefix); for (Entry> e : tailMap.entrySet()) { if (!e.getKey().startsWith(termPrefix)) { break; } var score = ((double) termPrefix.length()) / e.getKey().length(); e.getValue().stream() .map(v -> new SearchResult(score, v.obj, v.tokenIndex)) .forEach(entries::add); } return entries; } private void addToDictionary(String token, int tokenIndex, T obj) { var entries = termToObj.computeIfAbsent(token, t -> new LinkedList<>()); entries.add(new DictEntry(obj, tokenIndex)); } private List tokenize(String text) { var split = splitPattern.split(text); List terms = new ArrayList<>(split.length); for (String s : split) { if (!s.isEmpty()) { terms.add(s.toLowerCase(Locale.ENGLISH)); } } return terms; } public List getAll() { return List.copyOf(objToWordFrequency.keySet()); } private class SearchResult { private double score; private final T obj; private final int lastIdx; public SearchResult(double score, T obj, int lastIdx) { this.score = score; this.obj = obj; this.lastIdx = lastIdx; } } private class DictEntry { T obj; int tokenIndex; public DictEntry(T obj, int tokenIndex) { this.obj = obj; this.tokenIndex = tokenIndex; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/TokenGeneratorHelper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.embedded.server.AwaitingUserTokenFutureRepository; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.OpenUrlInBrowserParams; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static org.sonarsource.sonarlint.core.serverapi.UrlUtils.urlEncode; public class TokenGeneratorHelper { private final SonarLintRpcClient client; private final EmbeddedServer embeddedServer; private final AwaitingUserTokenFutureRepository awaitingUserTokenFutureRepository; private final String clientName; public TokenGeneratorHelper(SonarLintRpcClient client, EmbeddedServer embeddedServer, AwaitingUserTokenFutureRepository awaitingUserTokenFutureRepository, InitializeParams params) { this.client = client; this.embeddedServer = embeddedServer; this.awaitingUserTokenFutureRepository = awaitingUserTokenFutureRepository; this.clientName = params.getClientConstantInfo().getName(); } public HelpGenerateUserTokenResponse helpGenerateUserToken(String serverBaseUrl, @Nullable HelpGenerateUserTokenParams.Utm utm, SonarLintCancelMonitor cancelMonitor) { client.openUrlInBrowser(new OpenUrlInBrowserParams(ServerApiHelper.concat(serverBaseUrl, getUserTokenGenerationRelativeUrlToOpen(utm)))); var shouldWaitIncomingToken = embeddedServer.isStarted(); if (shouldWaitIncomingToken) { var future = new CompletableFuture(); awaitingUserTokenFutureRepository.addExpectedResponse(serverBaseUrl, future); cancelMonitor.onCancel(() -> future.cancel(false)); return future.join(); } else { return new HelpGenerateUserTokenResponse(null); } } private String getUserTokenGenerationRelativeUrlToOpen(@Nullable HelpGenerateUserTokenParams.Utm utm) { var params = new StringBuilder("ideName=" + urlEncode(clientName) + (embeddedServer.isStarted() ? ("&port=" + embeddedServer.getPort()) : "")); if (utm != null) { params.append(String.format("&utm_medium=%s&utm_source=%s&utm_content=%s&utm_term=%s", urlEncode(utm.getMedium()), urlEncode(utm.getSource()), urlEncode(utm.getContent()), urlEncode(utm.getTerm()) )); } return "/sonarlint/auth?" + params; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/UserPaths.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.SonarLintUserHome; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; public class UserPaths { public static UserPaths from(InitializeParams initializeParams) { var userHome = computeUserHome(initializeParams.getSonarlintUserHome()); createFolderIfNeeded(userHome); var workDir = Optional.ofNullable(initializeParams.getWorkDir()).orElse(userHome.resolve("work")); createFolderIfNeeded(workDir); var storageRoot = Optional.ofNullable(initializeParams.getStorageRoot()).orElse(userHome.resolve("storage")); createFolderIfNeeded(storageRoot); return new UserPaths(userHome, workDir, storageRoot, initializeParams.getTelemetryConstantAttributes().getProductKey()); } static Path computeUserHome(@Nullable String clientUserHome) { if (clientUserHome != null) { return Paths.get(clientUserHome); } return SonarLintUserHome.get(); } private static void createFolderIfNeeded(Path path) { try { Files.createDirectories(path); } catch (IOException e) { throw new IllegalStateException("Cannot create directory '" + path + "'", e); } } private final Path userHome; private final Path workDir; private final Path storageRoot; private final String productKey; private UserPaths(Path userHome, Path workDir, Path storageRoot, String productKey) { this.userHome = userHome; this.workDir = workDir; this.storageRoot = storageRoot; this.productKey = productKey; } public Path getUserHome() { return userHome; } public Path getWorkDir() { return workDir; } public Path getStorageRoot() { return storageRoot; } public Path getHomeIdeSpecificDir(String intermediateDir) { return userHome.resolve(intermediateDir).resolve(productKey); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowSoonUnsupportedMessageParams; import org.sonarsource.sonarlint.core.serverconnection.VersionUtils; import org.sonarsource.sonarlint.core.sync.SynchronizationService; import org.springframework.context.event.EventListener; public class VersionSoonUnsupportedHelper { private static final String UNSUPPORTED_NOTIFICATION_ID = "sonarlint.unsupported.%s.%s.id"; private static final String NOTIFICATION_MESSAGE = "The version '%s' used by the current connection '%s' will be soon unsupported. " + "Please consider upgrading to the latest %s LTS version to ensure continued support and access to the latest features."; private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final ConfigurationRepository configRepository; private final ConnectionConfigurationRepository connectionRepository; private final SonarQubeClientManager sonarQubeClientManager; private final SynchronizationService synchronizationService; private final Map cacheConnectionIdPerVersion = new ConcurrentHashMap<>(); private final ExecutorServiceShutdownWatchable executorService; public VersionSoonUnsupportedHelper(SonarLintRpcClient client, ConfigurationRepository configRepository, SonarQubeClientManager sonarQubeClientManager, ConnectionConfigurationRepository connectionRepository, SynchronizationService synchronizationService) { this.client = client; this.configRepository = configRepository; this.connectionRepository = connectionRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.synchronizationService = synchronizationService; this.executorService = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadExecutor("Version Soon Unsupported Helper")); } @EventListener public void configurationScopesAdded(ConfigurationScopesAddedWithBindingEvent event) { var configScopeIds = event.getConfigScopeIds(); checkIfSoonUnsupportedOncePerConnection(configScopeIds); } @EventListener public void bindingConfigChanged(BindingConfigChangedEvent event) { var configScopeId = event.configScopeId(); var connectionId = event.newConfig().connectionId(); if (connectionId != null) { queueCheckIfSoonUnsupported(connectionId, configScopeId); } } private void checkIfSoonUnsupportedOncePerConnection(Set configScopeIds) { // We will check once per connection, and send the notification for the first config scope associated to this connection var oneConfigScopeIdPerConnection = new HashMap(); configScopeIds.forEach(configScopeId -> { var effectiveBinding = configRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isPresent()) { var connectionId = effectiveBinding.get().connectionId(); oneConfigScopeIdPerConnection.putIfAbsent(connectionId, configScopeId); } }); oneConfigScopeIdPerConnection.forEach(this::queueCheckIfSoonUnsupported); } private void queueCheckIfSoonUnsupported(String connectionId, String configScopeId) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); executorService.execute(() -> { try { var connection = connectionRepository.getConnectionById(connectionId); if (connection != null && connection.getKind() == ConnectionKind.SONARQUBE) { sonarQubeClientManager.withActiveClient(connectionId, serverApi -> { var version = synchronizationService.readOrSynchronizeServerVersion(connectionId, serverApi, cancelMonitor); var isCached = cacheConnectionIdPerVersion.containsKey(connectionId) && cacheConnectionIdPerVersion.get(connectionId).compareTo(version) == 0; if (!isCached && VersionUtils.isVersionSupportedDuringGracePeriod(version)) { client.showSoonUnsupportedMessage( new ShowSoonUnsupportedMessageParams( String.format(UNSUPPORTED_NOTIFICATION_ID, connectionId, version.getName()), configScopeId, String.format(NOTIFICATION_MESSAGE, version.getName(), connectionId, VersionUtils.getCurrentLts()))); LOG.debug(String.format("Connection '%s' with version '%s' is detected to be soon unsupported", connection.getConnectionId(), version.getName())); } cacheConnectionIdPerVersion.put(connectionId, version); }); } } catch (Exception e) { LOG.error("Error while checking if soon unsupported", e); } }); } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop version soon unsupported executor service in a timely manner"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/active/rules/ActiveRuleDetails.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.active.rules; import java.util.Map; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.rule.RuleKey; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; public record ActiveRuleDetails( String ruleKeyString, String languageKey, @Nullable Map params, @Nullable String fullTemplateRuleKey, IssueSeverity issueSeverity, RuleType type, CleanCodeAttribute cleanCodeAttribute, Map impacts, @Nullable VulnerabilityProbability vulnerabilityProbability) implements ActiveRule { public ActiveRuleDetails { if (params == null) { params = Map.of(); } } @Override public RuleKey ruleKey() { return RuleKey.parse(ruleKeyString); } @Override public String severity() { throw new UnsupportedOperationException("severity not supported in SonarLint"); } @Override public String language() { return languageKey(); } @Override public String param(String key) { return params().get(key); } @Override public String internalKey() { // This is a hack for old versions of CFamily (https://github.com/SonarSource/sonar-cpp/pull/1598) return ruleKey().rule(); } @Override public String templateRuleKey() { if (!StringUtils.isEmpty(fullTemplateRuleKey)) { // The SQ plugin API expect template rule key to be only the "rule" part of the key (without the repository key) var ruleKey = RuleKey.parse(fullTemplateRuleKey); return ruleKey.rule(); } return null; } @Override public String qpKey() { throw new UnsupportedOperationException("qpKey not supported in SonarLint"); } @Override public String toString() { var sb = new StringBuilder(); sb.append(ruleKeyString()); if (!params.isEmpty()) { sb.append(params); } return sb.toString(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/active/rules/ActiveRulesService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.active.rules; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.RuleKey; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.EffectiveRuleDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetStandaloneRuleDescriptionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.StandaloneRuleConfigDto; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.rules.RuleDetails; import org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter; import org.sonarsource.sonarlint.core.rules.RuleNotFoundException; import org.sonarsource.sonarlint.core.rules.RulesService; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.push.RuleSetChangedEvent; import org.sonarsource.sonarlint.core.serverapi.rules.ServerActiveRule; import org.sonarsource.sonarlint.core.serverapi.rules.ServerRule; import org.sonarsource.sonarlint.core.serverconnection.AnalyzerConfiguration; import org.sonarsource.sonarlint.core.serverconnection.RuleSet; import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent; import org.sonarsource.sonarlint.core.serverconnection.storage.StorageException; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.AnalyzerConfigurationSynchronized; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Optional.ofNullable; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.sonarsource.sonarlint.core.commons.CleanCodeAttribute.CONVENTIONAL; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SECURITY_HOTSPOTS; import static org.sonarsource.sonarlint.core.rules.RulesService.COULD_NOT_FIND_RULE; import static org.sonarsource.sonarlint.core.rules.RulesService.IN_EMBEDDED_RULES; public class ActiveRulesService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configurationRepository; private final LanguageSupportRepository languageSupportRepository; private final SonarQubeClientManager sonarQubeClientManager; private final SeverityModeService severityModeService; private final StorageService storageService; private final RulesRepository rulesRepository; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final boolean hotspotEnabled; private final ApplicationEventPublisher eventPublisher; private final Map standaloneRuleConfig = new ConcurrentHashMap<>(); private List standaloneActiveRules; private final Map> activeRulesPerBinding = new ConcurrentHashMap<>(); public ActiveRulesService(ConfigurationRepository configurationRepository, LanguageSupportRepository languageSupportRepository, SonarQubeClientManager sonarQubeClientManager, SeverityModeService severityModeService, StorageService storageService, RulesRepository rulesRepository, ConnectionConfigurationRepository connectionConfigurationRepository, InitializeParams initializeParams, ApplicationEventPublisher eventPublisher) { this.configurationRepository = configurationRepository; this.languageSupportRepository = languageSupportRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.severityModeService = severityModeService; this.storageService = storageService; this.rulesRepository = rulesRepository; this.connectionConfigurationRepository = connectionConfigurationRepository; this.hotspotEnabled = initializeParams.getBackendCapabilities().contains(SECURITY_HOTSPOTS); this.eventPublisher = eventPublisher; this.standaloneRuleConfig.putAll(initializeParams.getStandaloneRuleConfigByKey()); } public void updateStandaloneRulesConfiguration(Map standaloneRuleConfig) { this.standaloneRuleConfig.clear(); this.standaloneRuleConfig.putAll(standaloneRuleConfig); this.standaloneActiveRules = null; eventPublisher.publishEvent(new StandaloneRulesConfigurationChanged(standaloneRuleConfig)); } public Map getStandaloneRuleConfig() { return Collections.unmodifiableMap(standaloneRuleConfig); } public GetStandaloneRuleDescriptionResponse getStandaloneRuleDescription(String ruleKey) { var embeddedRule = rulesRepository.getEmbeddedRule(ruleKey); if (embeddedRule.isEmpty()) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, COULD_NOT_FIND_RULE + ruleKey + IN_EMBEDDED_RULES, new Object[] {ruleKey}); throw new ResponseErrorException(error); } var ruleDefinition = embeddedRule.get(); var ruleDetails = RuleDetails.from(ruleDefinition, standaloneRuleConfig.get(ruleKey)); return new GetStandaloneRuleDescriptionResponse(RulesService.convert(ruleDefinition), RuleDetailsAdapter.transformDescriptions(ruleDetails, null)); } public synchronized List getStandaloneActiveRules() { if (standaloneActiveRules == null) { standaloneActiveRules = buildStandaloneActiveRules(); } return standaloneActiveRules; } private List buildStandaloneActiveRules() { Set excludedRules = standaloneRuleConfig.entrySet().stream() .filter(not(e -> e.getValue().isActive())) .map(Map.Entry::getKey).collect(toSet()); Set includedRules = standaloneRuleConfig.entrySet().stream() .filter(e -> e.getValue().isActive()) .map(Map.Entry::getKey) .filter(r -> !excludedRules.contains(r)) .collect(toSet()); var filteredActiveRules = new ArrayList(); var allRulesDefinitions = rulesRepository.getEmbeddedRules().stream() .filter(rule -> !rule.getType().equals(RuleType.SECURITY_HOTSPOT)) .toList(); filteredActiveRules.addAll(allRulesDefinitions.stream() .filter(SonarLintRuleDefinition::isActiveByDefault) .filter(isExcludedByConfiguration(excludedRules)) .toList()); filteredActiveRules.addAll(allRulesDefinitions.stream() .filter(r -> !r.isActiveByDefault()) .filter(isIncludedByConfiguration(includedRules)) .toList()); return filteredActiveRules.stream().map(ruleDefinition -> { Map effectiveParams = new HashMap<>(ruleDefinition.getDefaultParams()); ofNullable(standaloneRuleConfig.get(ruleDefinition.getKey())).ifPresent(config -> effectiveParams.putAll(config.getParamValueByKey())); // No template rules in standalone mode return new ActiveRuleDetails(ruleDefinition.getKey(), ruleDefinition.getLanguage().getSonarLanguageKey(), effectiveParams, null, ruleDefinition.getDefaultSeverity(), ruleDefinition.getType(), ruleDefinition.getCleanCodeAttribute().orElse(CONVENTIONAL), ruleDefinition.getDefaultImpacts(), ruleDefinition.getVulnerabilityProbability().orElse(null)); }) .toList(); } public List getConnectedActiveRules(Binding binding) { return activeRulesPerBinding.computeIfAbsent(binding, k -> buildConnectedActiveRules(binding)); } private List buildConnectedActiveRules(Binding binding) { var analyzerConfig = storageService.binding(binding).analyzerConfiguration().read(); var ruleSetByLanguageKey = analyzerConfig.getRuleSetByLanguageKey(); var result = new ArrayList(); ruleSetByLanguageKey.entrySet() .stream().filter(e -> SonarLanguage.forKey(e.getKey()).filter(l -> languageSupportRepository.getEnabledLanguagesInConnectedMode().contains(l)).isPresent()) .forEach(e -> { var languageKey = e.getKey(); var ruleSet = e.getValue(); LOG.debug(" * {}: {} active rules", languageKey, ruleSet.getRules().size()); var missingRuleOrTemplateDefinitions = new LinkedHashSet<>(); for (ServerActiveRule possiblyDeprecatedActiveRuleFromStorage : ruleSet.getRules()) { var activeRuleFromStorage = tryConvertDeprecatedKeys(binding.connectionId(), possiblyDeprecatedActiveRuleFromStorage); SonarLintRuleDefinition ruleOrTemplateDefinition; if (StringUtils.isNotBlank(activeRuleFromStorage.getTemplateKey())) { ruleOrTemplateDefinition = rulesRepository.getRule(binding.connectionId(), activeRuleFromStorage.getTemplateKey()).orElse(null); if (ruleOrTemplateDefinition == null) { LOG.debug("Rule {} is enabled on the server, but its template {} is not available in SonarLint", activeRuleFromStorage.getRuleKey(), activeRuleFromStorage.getTemplateKey()); continue; } } else { ruleOrTemplateDefinition = rulesRepository.getRule(binding.connectionId(), activeRuleFromStorage.getRuleKey()).orElse(null); if (ruleOrTemplateDefinition == null) { missingRuleOrTemplateDefinitions.add(activeRuleFromStorage.getRuleKey()); continue; } } if (shouldIncludeRuleForAnalysis(binding.connectionId(), ruleOrTemplateDefinition)) { result.add(buildActiveRule(ruleOrTemplateDefinition, activeRuleFromStorage)); } } if (!missingRuleOrTemplateDefinitions.isEmpty()) { LOG.debug("The following rules are enabled on the server, but not available in SonarLint: {}", missingRuleOrTemplateDefinitions); } }); if (languageSupportRepository.getEnabledLanguagesInConnectedMode().contains(SonarLanguage.IPYTHON)) { // Jupyter Notebooks are not yet fully supported in connected mode, use standalone rule configuration in the meantime var iPythonRules = getStandaloneActiveRules() .stream().filter(rule -> rule.languageKey().equals(SonarLanguage.IPYTHON.getSonarLanguageKey())) .toList(); result.addAll(iPythonRules); } return result; } public ActiveRuleDetails buildActiveRule(SonarLintRuleDefinition ruleOrTemplateDefinition, ServerActiveRule activeRule) { return new ActiveRuleDetails(activeRule.getRuleKey(), ruleOrTemplateDefinition.getLanguage().getSonarLanguageKey(), getEffectiveParams(ruleOrTemplateDefinition, activeRule), trimToNull(activeRule.getTemplateKey()), activeRule.getSeverity(), ruleOrTemplateDefinition.getType(), ruleOrTemplateDefinition.getCleanCodeAttribute().orElse(CONVENTIONAL), RuleDetails.mergeImpacts(ruleOrTemplateDefinition.getDefaultImpacts(), activeRule.getOverriddenImpacts()), ruleOrTemplateDefinition.getVulnerabilityProbability().orElse(null)); } private static Map getEffectiveParams(SonarLintRuleDefinition ruleOrTemplateDefinition, ServerActiveRule activeRule) { Map effectiveParams = new HashMap<>(ruleOrTemplateDefinition.getDefaultParams()); activeRule.getParams().forEach((paramName, paramValue) -> { if (!ruleOrTemplateDefinition.getParams().containsKey(paramName)) { LOG.debug("Rule parameter '{}' for rule '{}' does not exist in embedded analyzer, ignoring.", paramName, ruleOrTemplateDefinition.getKey()); return; } effectiveParams.put(paramName, paramValue); }); return effectiveParams; } private boolean shouldIncludeRuleForAnalysis(String connectionId, SonarLintRuleDefinition ruleDefinition) { var isHotspot = ruleDefinition.getType().equals(RuleType.SECURITY_HOTSPOT); return !isHotspot || (hotspotEnabled && isHotspotTrackingPossible(connectionId)); } private boolean isHotspotTrackingPossible(String connectionId) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); if (connection == null) { // Connection is gone return false; } // when storage is not present, consider hotspots should not be detected return storageService.connection(connectionId).serverInfo().read().isPresent(); } private static Predicate isExcludedByConfiguration(Set excludedRules) { return r -> { if (excludedRules.contains(r.getKey())) { return false; } for (String deprecatedKey : r.getDeprecatedKeys()) { if (excludedRules.contains(deprecatedKey)) { LOG.warn("Rule '{}' was excluded using its deprecated key '{}'. Please fix your configuration.", r.getKey(), deprecatedKey); return false; } } return true; }; } private static Predicate isIncludedByConfiguration(Set includedRules) { return r -> { if (includedRules.contains(r.getKey())) { return true; } for (String deprecatedKey : r.getDeprecatedKeys()) { if (includedRules.contains(deprecatedKey)) { LOG.warn("Rule '{}' was included using its deprecated key '{}'. Please fix your configuration.", r.getKey(), deprecatedKey); return true; } } return false; }; } public void evictFor(String connectionId) { LOG.debug("Evict cached active rules for connection '{}'", connectionId); activeRulesPerBinding.entrySet().removeIf( entry -> entry.getKey().connectionId().equals(connectionId) ); } @EventListener public void settingsChanged(SonarServerSettingsChangedEvent event) { // settings have an impact on rule definitions rulesRepository.evictFor(event.connectionId()); evictFor(event.connectionId()); } @EventListener public void onBindingUpdated(BindingConfigChangedEvent event) { var previousBinding = event.previousConfig(); var previousConnectionId = previousBinding.connectionId(); var previousProjectKey = previousBinding.sonarProjectKey(); if (previousConnectionId != null && previousProjectKey != null && configurationRepository.getBoundScopesToConnectionAndSonarProject(previousConnectionId, previousProjectKey).isEmpty()) { // evict the cache, active rules will be lazily loaded next time they are needed activeRulesPerBinding.remove(new Binding(previousConnectionId, previousProjectKey)); } } @EventListener public void onAnalyzerConfigurationSynchronized(AnalyzerConfigurationSynchronized event) { // evict the cache, active rules will be lazily loaded next time they are needed activeRulesPerBinding.remove(event.binding()); } @EventListener public void onServerEventReceived(SonarServerEventReceivedEvent eventReceived) { var connectionId = eventReceived.getConnectionId(); var serverEvent = eventReceived.getEvent(); if (serverEvent instanceof RuleSetChangedEvent ruleSetChangedEvent) { // evict the cache, active rules will be lazily loaded next time they are needed ruleSetChangedEvent.getProjectKeys().forEach(projectKey -> activeRulesPerBinding.remove(new Binding(connectionId, projectKey))); updateStorage(connectionId, ruleSetChangedEvent); eventPublisher.publishEvent( new ServerActiveRulesChanged(connectionId, ruleSetChangedEvent.getProjectKeys(), ruleSetChangedEvent.getActivatedRules(), ruleSetChangedEvent.getDeactivatedRules())); } } private void updateStorage(String connectionId, RuleSetChangedEvent event) { event.getProjectKeys().forEach(projectKey -> storageService.connection(connectionId).project(projectKey).analyzerConfiguration().update(currentConfiguration -> { var newRuleSetByLanguageKey = incorporate(event, currentConfiguration.getRuleSetByLanguageKey()); return new AnalyzerConfiguration(currentConfiguration.getSettings(), newRuleSetByLanguageKey, currentConfiguration.getSchemaVersion()); })); } private static Map incorporate(RuleSetChangedEvent event, Map ruleSetByLanguageKey) { Map resultingRuleSetsByLanguageKey = new HashMap<>(ruleSetByLanguageKey); event.getDeactivatedRules().forEach(deactivatedRule -> deactivate(deactivatedRule, resultingRuleSetsByLanguageKey)); event.getActivatedRules().forEach(activatedRule -> activate(activatedRule, resultingRuleSetsByLanguageKey)); return resultingRuleSetsByLanguageKey; } private static void activate(RuleSetChangedEvent.ActiveRule activatedRule, Map ruleSetsByLanguageKey) { var ruleLanguageKey = activatedRule.getLanguageKey(); var currentRuleSet = ruleSetsByLanguageKey.computeIfAbsent(ruleLanguageKey, k -> new RuleSet(Collections.emptyList(), "")); var languageRulesByKey = new HashMap<>(currentRuleSet.getRulesByKey()); var ruleTemplateKey = activatedRule.getTemplateKey(); languageRulesByKey.put(activatedRule.getKey(), new ServerActiveRule( activatedRule.getKey(), activatedRule.getSeverity(), activatedRule.getParameters(), ruleTemplateKey == null ? "" : ruleTemplateKey, activatedRule.getOverriddenImpacts())); ruleSetsByLanguageKey.put(ruleLanguageKey, new RuleSet(new ArrayList<>(languageRulesByKey.values()), currentRuleSet.getLastModified())); } private static void deactivate(String deactivatedRuleKey, Map ruleSetsByLanguageKey) { var ruleSetsIterator = ruleSetsByLanguageKey.entrySet().iterator(); while (ruleSetsIterator.hasNext()) { var ruleSetEntry = ruleSetsIterator.next(); var ruleSet = ruleSetEntry.getValue(); var newRules = new HashMap<>(ruleSet.getRulesByKey()); newRules.remove(deactivatedRuleKey); if (newRules.isEmpty()) { ruleSetsIterator.remove(); } else { ruleSetsByLanguageKey.put(ruleSetEntry.getKey(), new RuleSet(List.copyOf(newRules.values()), ruleSet.getLastModified())); } } } public EffectiveRuleDetailsDto getEffectiveRuleDetails(String configurationScopeId, String ruleKey, @Nullable String contextKey, SonarLintCancelMonitor cancelMonitor) throws RuleNotFoundException { var ruleDetails = getActiveRuleDetails(configurationScopeId, ruleKey, cancelMonitor); return RuleDetailsAdapter.transform(ruleDetails, contextKey); } public RuleDetails getActiveRuleDetails(String configurationScopeId, String ruleKey, SonarLintCancelMonitor cancelMonitor) throws RuleNotFoundException { var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); RuleDetails ruleDetails; if (effectiveBinding.isEmpty()) { var embeddedRule = rulesRepository.getEmbeddedRule(ruleKey); if (embeddedRule.isEmpty()) { throw new RuleNotFoundException(COULD_NOT_FIND_RULE + ruleKey + IN_EMBEDDED_RULES, ruleKey); } ruleDetails = RuleDetails.from(embeddedRule.get(), standaloneRuleConfig.get(ruleKey)); } else { ruleDetails = getActiveRuleForBinding(ruleKey, effectiveBinding.get(), cancelMonitor); } return ruleDetails; } public synchronized void evictStandalone() { LOG.debug("Evict cached standalone active rules"); standaloneActiveRules = null; } private RuleDetails getActiveRuleForBinding(String ruleKey, Binding binding, SonarLintCancelMonitor cancelMonitor) { var connectionId = binding.connectionId(); sonarQubeClientManager.getValidClientOrThrow(connectionId); var serverUsesStandardSeverityMode = !severityModeService.isMQRModeForConnection(connectionId); return findServerActiveRuleInStorage(binding, ruleKey) .map(storageRule -> hydrateDetailsWithServer(connectionId, storageRule, serverUsesStandardSeverityMode, cancelMonitor)) // try from loaded rules, for e.g. extra analyzers .orElseGet(() -> rulesRepository.getRule(connectionId, ruleKey) .map(r -> RuleDetails.from(r, standaloneRuleConfig.get(ruleKey))) .orElseThrow(() -> ruleNotFoundInPlugins(ruleKey, connectionId))); } private Optional findServerActiveRuleInStorage(Binding binding, String ruleKey) { AnalyzerConfiguration analyzerConfiguration; try { analyzerConfiguration = storageService.binding(binding).analyzerConfiguration().read(); } catch (StorageException e) { // XXX we should make sure this situation can not happen (sync should be enforced at least once) return Optional.empty(); } return analyzerConfiguration.getRuleSetByLanguageKey().values().stream() .flatMap(s -> s.getRules().stream()) // XXX is it important to migrate the rule repos in tryConvertDeprecatedKeys? .filter(r -> tryConvertDeprecatedKeys(binding.connectionId(), r).getRuleKey().equals(ruleKey)).findFirst(); } private ServerActiveRule tryConvertDeprecatedKeys(String connectionId, ServerActiveRule possiblyDeprecatedActiveRuleFromStorage) { SonarLintRuleDefinition ruleOrTemplateDefinition; if (StringUtils.isNotBlank(possiblyDeprecatedActiveRuleFromStorage.getTemplateKey())) { ruleOrTemplateDefinition = rulesRepository.getRule(connectionId, possiblyDeprecatedActiveRuleFromStorage.getTemplateKey()).orElse(null); if (ruleOrTemplateDefinition == null) { // The rule template is not known among our loaded analyzers, so return it untouched, to let calling code take appropriate decision return possiblyDeprecatedActiveRuleFromStorage; } var ruleKeyPossiblyWithDeprecatedRepo = RuleKey.parse(possiblyDeprecatedActiveRuleFromStorage.getRuleKey()); var templateRuleKeyWithCorrectRepo = RuleKey.parse(ruleOrTemplateDefinition.getKey()); var ruleKey = new RuleKey(templateRuleKeyWithCorrectRepo.repository(), ruleKeyPossiblyWithDeprecatedRepo.rule()).toString(); return new ServerActiveRule(ruleKey, possiblyDeprecatedActiveRuleFromStorage.getSeverity(), possiblyDeprecatedActiveRuleFromStorage.getParams(), ruleOrTemplateDefinition.getKey(), possiblyDeprecatedActiveRuleFromStorage.getOverriddenImpacts()); } else { ruleOrTemplateDefinition = rulesRepository.getRule(connectionId, possiblyDeprecatedActiveRuleFromStorage.getRuleKey()).orElse(null); if (ruleOrTemplateDefinition == null) { // The rule is not known among our loaded analyzers, so return it untouched, to let calling code take appropriate decision return possiblyDeprecatedActiveRuleFromStorage; } return new ServerActiveRule(ruleOrTemplateDefinition.getKey(), possiblyDeprecatedActiveRuleFromStorage.getSeverity(), possiblyDeprecatedActiveRuleFromStorage.getParams(), null, possiblyDeprecatedActiveRuleFromStorage.getOverriddenImpacts()); } } private RuleDetails hydrateDetailsWithServer(String connectionId, ServerActiveRule activeRuleFromStorage, boolean skipCleanCodeTaxonomy, SonarLintCancelMonitor cancelMonitor) { var ruleKey = activeRuleFromStorage.getRuleKey(); var templateKey = activeRuleFromStorage.getTemplateKey(); var serverConnection = sonarQubeClientManager.getValidClientOrThrow(connectionId); if (StringUtils.isNotBlank(templateKey)) { var templateRule = rulesRepository.getRule(connectionId, templateKey); if (templateRule.isEmpty()) { throw ruleDefinitionNotFound(templateKey); } var serverRule = serverConnection.withClientApiAndReturn(serverApi -> fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor)); return RuleDetails.merging(activeRuleFromStorage, serverRule, templateRule.get(), skipCleanCodeTaxonomy); } else { var serverRule = serverConnection.withClientApiAndReturn(serverApi -> fetchRuleFromServer(connectionId, ruleKey, serverApi, cancelMonitor)); var ruleDefFromPluginOpt = rulesRepository.getRule(connectionId, ruleKey); return ruleDefFromPluginOpt .map(ruleDefFromPlugin -> RuleDetails.merging(serverRule, ruleDefFromPlugin, skipCleanCodeTaxonomy)) .orElseGet(() -> RuleDetails.merging(activeRuleFromStorage, serverRule)); } } private static ServerRule fetchRuleFromServer(String connectionId, String ruleKey, ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { return serverApi.rules().getRule(ruleKey, cancelMonitor).orElseThrow(() -> ruleNotFoundOnServer(ruleKey, connectionId)); } private static ResponseErrorException ruleDefinitionNotFound(String templateKey) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, "Unable to find rule definition for rule template " + templateKey, templateKey); return new ResponseErrorException(error); } @NotNull private static ResponseErrorException ruleNotFoundInPlugins(String ruleKey, String connectionId) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, COULD_NOT_FIND_RULE + ruleKey + "' in plugins loaded from '" + connectionId + "'", new Object[] {connectionId, ruleKey}); return new ResponseErrorException(error); } private static ResponseErrorException ruleNotFoundOnServer(String ruleKey, String connectionId) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, COULD_NOT_FIND_RULE + ruleKey + "' on server '" + connectionId + "'", new Object[] {connectionId, ruleKey}); return new ResponseErrorException(error); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/active/rules/ServerActiveRulesChanged.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.active.rules; import java.util.List; import org.sonarsource.sonarlint.core.serverapi.push.RuleSetChangedEvent; public record ServerActiveRulesChanged(String connectionId, List projectKeys, List activatedRules, List deactivatedRules) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/active/rules/StandaloneRulesConfigurationChanged.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.active.rules; import java.util.List; import java.util.Map; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.StandaloneRuleConfigDto; public class StandaloneRulesConfigurationChanged { private final Map standaloneRuleConfig; StandaloneRulesConfigurationChanged(Map standaloneRuleConfig) { this.standaloneRuleConfig = standaloneRuleConfig; } public boolean isOnlyDeactivated() { return standaloneRuleConfig.values().stream() .noneMatch(StandaloneRuleConfigDto::isActive); } public List getDeactivatedRules() { return standaloneRuleConfig.entrySet().stream() .filter(entry -> !entry.getValue().isActive()) .map(Map.Entry::getKey) .toList(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/active/rules/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.active.rules; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ai/ide/AiAgentService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; import jakarta.inject.Inject; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetRuleFileContentResponse; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; public class AiAgentService { private final TelemetryService telemetryService; @Inject public AiAgentService(TelemetryService telemetryService) { this.telemetryService = telemetryService; } public GetRuleFileContentResponse getRuleFileContent(AiAgent agent) { var header = switch (agent) { case CURSOR, WINDSURF -> """ --- description: SonarQube MCP Server usage guidelines globs: alwaysApply: true --- """; case KIRO -> """ --- inclusion: always --- """; case GITHUB_COPILOT -> """ --- applyTo: "**/*" --- """; }; var response = new GetRuleFileContentResponse(header + """ These are some guidelines when using the SonarQube MCP server. # Important Tool Guidelines ## Basic usage - **IMPORTANT**: After you finish generating or modifying any code files at the very end of the task, you MUST call the `analyze_file_list` \ tool (if it exists) to analyze the files you created or modified. - **IMPORTANT**: When starting a new task, you MUST disable automatic analysis with the `toggle_automatic_analysis` tool if it exists. - **IMPORTANT**: When you are done generating code at the very end of the task, \ you MUST re-enable automatic analysis with the `toggle_automatic_analysis` tool if it exists. ## Project Keys - When a user mentions a project key, use `search_my_sonarqube_projects` first to find the exact project key - Don't guess project keys - always look them up ## Code Language Detection - When analyzing code snippets, try to detect the programming language from the code syntax - If unclear, ask the user or make an educated guess based on syntax ## Branch and Pull Request Context - Many operations support branch-specific analysis - If user mentions working on a feature branch, include the branch parameter ## Code Issues and Violations - After fixing issues, do not attempt to verify them using `search_sonar_issues_in_projects`, as the server will not yet reflect the updates # Common Troubleshooting ## Authentication Issues - SonarQube requires USER tokens (not project tokens) - When the error `SonarQube answered with Not authorized` occurs, verify the token type ## Project Not Found - Use `search_my_sonarqube_projects` to find available projects - Verify project key spelling and format ## Code Analysis Issues - Ensure programming language is correctly specified - Remind users that snippet analysis doesn't replace full project scans - Provide full file content for better analysis results """); telemetryService.mcpRuleFileRequested(); return response; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ai/ide/AiHookService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; import jakarta.inject.Inject; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetHookScriptContentResponse; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; public class AiHookService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String WINDSURF_HOOK_CONFIG = """ { "hooks": { "post_write_code": [ { "command": "{{SCRIPT_PATH}}", "show_output": true } ] } } """; private final EmbeddedServer embeddedServer; private final ExecutableLocator executableLocator; private final TelemetryService telemetryService; @Inject public AiHookService(EmbeddedServer embeddedServer, TelemetryService telemetryService) { this(embeddedServer, telemetryService, new ExecutableLocator()); } // For testing AiHookService(EmbeddedServer embeddedServer, TelemetryService telemetryService, ExecutableLocator executableLocator) { this.embeddedServer = embeddedServer; this.telemetryService = telemetryService; this.executableLocator = executableLocator; } public GetHookScriptContentResponse getHookScriptContent(AiAgent agent) { var port = embeddedServer.getPort(); if (port <= 0) { throw new IllegalStateException("Embedded server is not started. Cannot generate hook script."); } var hookScriptType = executableLocator.detectBestExecutable() .orElseThrow(() -> new IllegalStateException("No suitable executable found for hook script generation. " + "Please ensure Node.js, Python, or Bash is available on your system.")); var scriptContent = loadTemplateAndReplacePlaceholders(hookScriptType.getFileName(), port, agent); var configContent = generateHookConfiguration(agent); var configFileName = getConfigFileName(agent); telemetryService.aiHookInstalled(agent); return new GetHookScriptContentResponse(scriptContent, hookScriptType.getFileName(), configContent, configFileName); } private static String generateHookConfiguration(AiAgent agent) { return switch (agent) { case WINDSURF -> WINDSURF_HOOK_CONFIG; case CURSOR, KIRO -> throw new UnsupportedOperationException(agent + " hook configuration not yet implemented"); case GITHUB_COPILOT -> throw new UnsupportedOperationException("GitHub Copilot does not support hooks"); }; } private static String getConfigFileName(AiAgent agent) { return switch (agent) { case WINDSURF -> "hooks.json"; case CURSOR, KIRO -> throw new UnsupportedOperationException(agent + " hook configuration not yet implemented"); case GITHUB_COPILOT -> throw new UnsupportedOperationException("GitHub Copilot does not support hooks"); }; } private static String loadTemplateAndReplacePlaceholders(String templateFileName, int port, AiAgent agent) { var resourcePath = "/ai/hooks/" + templateFileName; try (var inputStream = AiHookService.class.getResourceAsStream(resourcePath)) { if (inputStream == null) { throw new IllegalStateException("Hook script template not found: " + resourcePath); } var template = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); return template .replace("{{PORT}}", String.valueOf(port)) .replace("{{AGENT}}", getIdeName(agent)); } catch (IOException e) { LOG.error("Failed to load hook script template: {}", templateFileName, e); throw new IllegalStateException("Failed to load hook script template: " + templateFileName, e); } } private static String getIdeName(AiAgent agent) { return switch (agent) { case WINDSURF -> "Windsurf"; case CURSOR -> "Cursor"; case KIRO -> "Kiro"; case GITHUB_COPILOT -> throw new UnsupportedOperationException("GitHub Copilot does not support hooks"); }; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ai/ide/ExecutableLocator.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Optional; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandException; import org.sonar.api.utils.command.CommandExecutor; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.nodejs.NodeJsHelper; public class ExecutableLocator { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final System2 system2; private final Path pathHelperLocationOnMac; private final CommandExecutor commandExecutor; private final NodeJsHelper nodeJsHelper; private boolean checkedForExecutable = false; private HookScriptType detectedExecutable = null; public ExecutableLocator() { this(System2.INSTANCE, Paths.get("/usr/libexec/path_helper"), CommandExecutor.create(), new NodeJsHelper()); } // For testing ExecutableLocator(System2 system2, Path pathHelperLocationOnMac, CommandExecutor commandExecutor, NodeJsHelper nodeJsHelper) { this.system2 = system2; this.pathHelperLocationOnMac = pathHelperLocationOnMac; this.commandExecutor = commandExecutor; this.nodeJsHelper = nodeJsHelper; } public Optional detectBestExecutable() { if (checkedForExecutable) { return Optional.ofNullable(detectedExecutable); } // Priority: Node.js > Python > Bash if (isNodeJsAvailable()) { LOG.debug("Detected Node.js for hook scripts"); detectedExecutable = HookScriptType.NODEJS; } else if (isPythonAvailable()) { LOG.debug("Detected Python for hook scripts"); detectedExecutable = HookScriptType.PYTHON; } else if (isBashAvailable()) { LOG.debug("Detected Bash for hook scripts"); detectedExecutable = HookScriptType.BASH; } else { LOG.debug("No suitable executable found for hook scripts"); detectedExecutable = null; } checkedForExecutable = true; return Optional.ofNullable(detectedExecutable); } private boolean isNodeJsAvailable() { try { var installedNodeJs = nodeJsHelper.autoDetect(); return installedNodeJs != null; } catch (Exception e) { LOG.debug("Error detecting Node.js", e); return false; } } private boolean isPythonAvailable() { // Try python3 first, then python var python3Path = locatePythonExecutable("python3"); if (python3Path != null) { return true; } var pythonPath = locatePythonExecutable("python"); return pythonPath != null; } @CheckForNull private String locatePythonExecutable(String executable) { LOG.debug("Looking for {} in the PATH", executable); String result; if (system2.isOsWindows()) { result = runSimpleCommand(Command.create("C:\\Windows\\System32\\where.exe").addArgument("$PATH:" + executable + ".exe")); } else { var which = Command.create("/usr/bin/which").addArgument(executable); computePathEnvForMacOs(which); result = runSimpleCommand(which); } if (result != null) { LOG.debug("Found {} at {}", executable, result); return result; } else { LOG.debug("Unable to locate {}", executable); return null; } } private boolean isBashAvailable() { if (system2.isOsWindows()) { // On Windows, try to locate bash.exe (Git Bash, WSL, etc.) var bashPath = runSimpleCommand(Command.create("C:\\Windows\\System32\\where.exe").addArgument("$PATH:bash.exe")); return bashPath != null; } else { // On Unix/Mac, bash is always available return Files.exists(Paths.get("/bin/bash")); } } void computePathEnvForMacOs(Command command) { if (system2.isOsMac() && Files.exists(pathHelperLocationOnMac)) { var pathHelperCommand = Command.create(pathHelperLocationOnMac.toString()).addArgument("-s"); var pathHelperOutput = runSimpleCommand(pathHelperCommand); if (pathHelperOutput != null) { var regex = Pattern.compile("^\\s*PATH=\"([^\"]+)\"; export PATH;?\\s*$"); var matchResult = regex.matcher(pathHelperOutput); if (matchResult.matches()) { command.setEnvironmentVariable("PATH", matchResult.group(1)); } } } } @CheckForNull String runSimpleCommand(Command command) { var stdOut = new ArrayList(); var stdErr = new ArrayList(); LOG.debug("Execute command '{}'...", command); int exitCode; try { exitCode = commandExecutor.execute(command, stdOut::add, stdErr::add, 10_000); } catch (CommandException e) { LOG.debug("Unable to execute the command", e); return null; } var msg = new StringBuilder(String.format("Command '%s' exited with %s", command, exitCode)); if (!stdOut.isEmpty()) { msg.append("\nstdout: ").append(String.join("\n", stdOut)); } if (!stdErr.isEmpty()) { msg.append("\nstderr: ").append(String.join("\n", stdErr)); } LOG.debug("{}", msg); if (exitCode != 0 || stdOut.isEmpty()) { return null; } return stdOut.get(0); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ai/ide/HookScriptType.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; public enum HookScriptType { NODEJS("sonarqube_analysis_hook.js"), PYTHON("sonarqube_analysis_hook.py"), BASH("sonarqube_analysis_hook.sh"); private final String fileName; HookScriptType(String fileName) { this.fileName = fileName; } public String getFileName() { return fileName; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/ai/ide/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.ai.ide; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisFailedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.UUID; public record AnalysisFailedEvent(UUID analysisId) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisFinishedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.net.URI; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import static java.util.function.Predicate.not; public class AnalysisFinishedEvent { private final UUID analysisId; private final String configurationScopeId; private final Duration analysisDuration; private final Map languagePerFile; private final boolean succeededForAllFiles; private final List issues; private final Set reportedRuleKeys; private final Set detectedLanguages; private final boolean shouldFetchServerIssues; public AnalysisFinishedEvent(UUID analysisId, String configurationScopeId, Duration analysisDuration, Map languagePerFile, boolean succeededForAllFiles, List issues, boolean shouldFetchServerIssues) { this.analysisId = analysisId; this.configurationScopeId = configurationScopeId; this.analysisDuration = analysisDuration; this.languagePerFile = languagePerFile; this.succeededForAllFiles = succeededForAllFiles; this.issues = issues; this.reportedRuleKeys = issues.stream().map(RawIssue::getRuleKey).collect(Collectors.toSet()); this.detectedLanguages = languagePerFile.values().stream().filter(Objects::nonNull).collect(Collectors.toSet()); this.shouldFetchServerIssues = shouldFetchServerIssues; } public UUID getAnalysisId() { return analysisId; } public String getConfigurationScopeId() { return configurationScopeId; } public Duration getAnalysisDuration() { return analysisDuration; } public Map getLanguagePerFile() { return languagePerFile; } public boolean succeededForAllFiles() { return succeededForAllFiles; } public Set getReportedRuleKeys() { return reportedRuleKeys; } public Set getDetectedLanguages() { return detectedLanguages; } public List getIssues() { return issues.stream().filter(not(RawIssue::isSecurityHotspot)).toList(); } public List getHotspots() { return issues.stream().filter(RawIssue::isSecurityHotspot).toList(); } public boolean shouldFetchServerIssues() { return shouldFetchServerIssues; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisResult.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.net.URI; import java.util.List; import java.util.Set; public record AnalysisResult(Set failedAnalysisFiles, List rawIssues) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisSchedulerCache.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.nio.file.Path; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileSystem; import org.sonarsource.sonarlint.core.analysis.command.UnregisterModuleCommand; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.plugin.PluginLifecycleService; import org.sonarsource.sonarlint.core.plugin.PluginsConfiguration; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.springframework.context.event.EventListener; import static org.sonarsource.sonarlint.core.commons.tracing.Trace.startChild; public class AnalysisSchedulerCache { private final Path workDir; private final ClientFileSystemService clientFileSystemService; private final ConfigurationRepository configurationRepository; private final PluginsService pluginsService; private final PluginLifecycleService pluginLifecycleService; private final NodeJsService nodeJsService; private final AtomicReference standaloneScheduler = new AtomicReference<>(); private final ConcurrentHashMap connectedSchedulerByConnectionId = new ConcurrentHashMap<>(); public AnalysisSchedulerCache(UserPaths userPaths, ConfigurationRepository configurationRepository, NodeJsService nodeJsService, PluginsService pluginsService, PluginLifecycleService pluginLifecycleService, ClientFileSystemService clientFileSystemService) { this.configurationRepository = configurationRepository; this.pluginsService = pluginsService; this.pluginLifecycleService = pluginLifecycleService; this.nodeJsService = nodeJsService; this.workDir = userPaths.getWorkDir(); this.clientFileSystemService = clientFileSystemService; } @CheckForNull public AnalysisScheduler getAnalysisSchedulerIfStarted(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId) .map(binding -> getConnectedSchedulerIfStarted(binding.connectionId())) .orElseGet(this::getStandaloneSchedulerIfStarted); } public AnalysisScheduler getOrCreateAnalysisScheduler(String configurationScopeId) { return getOrCreateAnalysisScheduler(configurationScopeId, null); } public AnalysisScheduler getOrCreateAnalysisScheduler(String configurationScopeId, @Nullable Trace trace) { return configurationRepository.getEffectiveBinding(configurationScopeId) .map(binding -> getOrCreateConnectedScheduler(binding.connectionId(), trace)) .orElseGet(() -> getOrCreateStandaloneScheduler(trace)); } private synchronized AnalysisScheduler getOrCreateConnectedScheduler(String connectionId, @Nullable Trace trace) { return connectedSchedulerByConnectionId.computeIfAbsent(connectionId, k -> createScheduler(pluginsService.getPlugins(connectionId), trace)); } @CheckForNull private synchronized AnalysisScheduler getConnectedSchedulerIfStarted(String connectionId) { return connectedSchedulerByConnectionId.get(connectionId); } private synchronized AnalysisScheduler getOrCreateStandaloneScheduler(@Nullable Trace trace) { var scheduler = standaloneScheduler.get(); if (scheduler == null) { scheduler = createScheduler(pluginsService.getEmbeddedPlugins(), trace); standaloneScheduler.set(scheduler); } return scheduler; } @CheckForNull private synchronized AnalysisScheduler getStandaloneSchedulerIfStarted() { return standaloneScheduler.get(); } private AnalysisScheduler createScheduler(PluginsConfiguration pluginsConfiguration, @Nullable Trace trace) { var config = buildSchedulerConfiguration(pluginsConfiguration.extraProperties(), trace); return new AnalysisScheduler(config, pluginsConfiguration.plugins(), SonarLintLogger.get().getTargetForCopy()); } private AnalysisSchedulerConfiguration buildSchedulerConfiguration(Map extraProperties, @Nullable Trace trace) { var activeNodeJs = startChild(trace, "getActiveNodeJs", "createSchedulerConfiguration", nodeJsService::getActiveNodeJs); var nodeJsPath = activeNodeJs == null ? null : activeNodeJs.getPath(); return AnalysisSchedulerConfiguration.builder() .setWorkDir(workDir) .setClientPid(ProcessHandle.current().pid()) .setExtraProperties(extraProperties) .setNodeJs(nodeJsPath) .setFileSystemProvider(this::getFileSystem) .build(); } private SchedulerResetConfiguration toSchedulerResetConfiguration(PluginsConfiguration pluginsConfiguration) { return new SchedulerResetConfiguration(buildSchedulerConfiguration(pluginsConfiguration.extraProperties(), null), pluginsConfiguration.plugins()); } private ClientModuleFileSystem getFileSystem(String configurationScopeId) { return new BackendModuleFileSystem(clientFileSystemService, configurationScopeId); } @EventListener public void onConnectionRemoved(ConnectionConfigurationRemovedEvent event) { stop(event.removedConnectionId()); } public synchronized void reloadPlugins(String connectionId) { var scheduler = connectedSchedulerByConnectionId.get(connectionId); if (scheduler != null) { scheduler.reset(() -> toSchedulerResetConfiguration(pluginLifecycleService.reloadPluginsAndEvictCaches(connectionId))); } else { // Scheduler doesn't exist yet (lazy initialization), but still need to unload old plugins and evict caches // This ensures that when the scheduler is eventually created, it won't use stale cached data pluginLifecycleService.unloadPluginsAndEvictCaches(connectionId); } } public synchronized void reloadStandalonePlugins() { var scheduler = standaloneScheduler.get(); if (scheduler != null) { scheduler.reset(() -> toSchedulerResetConfiguration(pluginLifecycleService.reloadEmbeddedPluginsAndEvictCaches())); } else { pluginLifecycleService.unloadEmbeddedPluginsAndEvictCaches(); } } @EventListener public void onClientNodeJsPathChanged(ClientNodeJsPathChanged event) { resetStartedSchedulers(); } @EventListener public void onBindingConfigurationChanged(BindingConfigChangedEvent event) { var schedulerBeforeBindingChange = event.previousConfig().isBound() ? getConnectedSchedulerIfStarted(Objects.requireNonNull(event.previousConfig().connectionId())) : getStandaloneSchedulerIfStarted(); var schedulerAfterBindingChange = getAnalysisSchedulerIfStarted(event.configScopeId()); if (schedulerBeforeBindingChange != null && schedulerAfterBindingChange != schedulerBeforeBindingChange) { schedulerBeforeBindingChange.post(new UnregisterModuleCommand(event.configScopeId())); configurationRepository.getChildrenWithInheritedBinding(event.configScopeId()) .forEach(childId -> schedulerBeforeBindingChange.post(new UnregisterModuleCommand(childId))); } } @PreDestroy public void shutdown() { try { stopAll(); } catch (Exception e) { SonarLintLogger.get().error("Error shutting down analysis scheduler cache", e); } } private synchronized void resetStartedSchedulers() { var standaloneAnalysisScheduler = this.standaloneScheduler.get(); if (standaloneAnalysisScheduler != null) { standaloneAnalysisScheduler.reset(() -> toSchedulerResetConfiguration(pluginsService.getEmbeddedPlugins())); } connectedSchedulerByConnectionId.forEach( (connectionId, scheduler) -> scheduler.reset(() -> toSchedulerResetConfiguration(pluginsService.getPlugins(connectionId)))); } private synchronized void stopAll() { var standaloneAnalysisScheduler = this.standaloneScheduler.getAndSet(null); if (standaloneAnalysisScheduler != null) { standaloneAnalysisScheduler.stop(); } connectedSchedulerByConnectionId.forEach((connectionId, scheduler) -> scheduler.stop()); connectedSchedulerByConnectionId.clear(); } private synchronized void stop(String connectionId) { var scheduler = connectedSchedulerByConnectionId.remove(connectionId); if (scheduler != null) { scheduler.stop(); } pluginLifecycleService.unloadPluginsAndEvictCaches(connectionId); } public void unregisterModule(String scopeId, @Nullable String connectionId) { var analysisScheduler = connectionId == null ? getStandaloneSchedulerIfStarted() : getConnectedSchedulerIfStarted(connectionId); if (analysisScheduler != null) { if (connectionId != null && !configurationRepository.hasScopesBoundToConnection(connectionId)) { stop(connectionId); } else { analysisScheduler.post(new UnregisterModuleCommand(scopeId)); } } else if (connectionId != null && !configurationRepository.hasScopesBoundToConnection(connectionId)) { pluginLifecycleService.unloadPluginsAndEvictCaches(connectionId); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.BooleanUtils; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.active.rules.ActiveRuleDetails; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.active.rules.ServerActiveRulesChanged; import org.sonarsource.sonarlint.core.active.rules.StandaloneRulesConfigurationChanged; import org.sonarsource.sonarlint.core.analysis.api.AnalysisConfiguration; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileEvent; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand; import org.sonarsource.sonarlint.core.analysis.command.NotifyModuleEventCommand; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.PluginStatusUpdateEvent; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.fs.FileExclusionService; import org.sonarsource.sonarlint.core.fs.FileOpenedEvent; import org.sonarsource.sonarlint.core.fs.FileSystemUpdatedEvent; import org.sonarsource.sonarlint.core.fs.OpenFilesRepository; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.monitoring.MonitoringService; import org.sonarsource.sonarlint.core.nodejs.InstalledNodeJs; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.plugin.commons.MultivalueProperty; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.DidChangeAnalysisReadinessParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.DidDetectSecretParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetInferredAnalysisPropertiesParams; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.AnalyzerConfigurationSynchronized; import org.sonarsource.sonarlint.core.sync.ConfigurationScopesSynchronizedEvent; import org.sonarsource.sonarlint.core.sync.PluginsSynchronizedEvent; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.LanguageDetection.sanitizeExtension; import static org.sonarsource.sonarlint.core.commons.tracing.Trace.startChild; import static org.sonarsource.sonarlint.core.commons.util.StringUtils.pluralize; import static org.sonarsource.sonarlint.core.commons.util.git.GitService.getVCSChangedFiles; public class AnalysisService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONAR_INTERNAL_BUNDLE_PATH_ANALYSIS_PROP = "sonar.js.internal.bundlePath"; private static final String ANALYSIS_CFG_FOR_ENGINE = "getAnalysisConfigForEngine"; private static final String GET_ANALYSIS_CFG = "getAnalysisConfig"; private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final LanguageSupportRepository languageSupportRepository; private final StorageService storageService; private final PluginsService pluginsService; private final ActiveRulesService activeRulesService; private final ClientFileSystemService fileSystemService; private final FileExclusionService fileExclusionService; private final MonitoringService monitoringService; private final TaskManager taskManager; private final NodeJsService nodeJsService; private final AnalysisSchedulerCache schedulerCache; private final ApplicationEventPublisher eventPublisher; private final UserAnalysisPropertiesRepository userAnalysisPropertiesRepository; private final Map analysisReadinessByConfigScopeId = new ConcurrentHashMap<>(); private final OpenFilesRepository openFilesRepository; private final ClientFileSystemService clientFileSystemService; private final Path esLintBridgeServerPath; private boolean automaticAnalysisEnabled; public AnalysisService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, LanguageSupportRepository languageSupportRepository, StorageService storageService, PluginsService pluginsService, ActiveRulesService activeRulesService, ClientFileSystemService fileSystemService, FileExclusionService fileExclusionService, MonitoringService monitoringService, TaskManager taskManager, InitializeParams initializeParams, NodeJsService nodeJsService, AnalysisSchedulerCache schedulerCache, ApplicationEventPublisher eventPublisher, UserAnalysisPropertiesRepository clientAnalysisPropertiesRepository, OpenFilesRepository openFilesRepository, ClientFileSystemService clientFileSystemService) { this.client = client; this.configurationRepository = configurationRepository; this.languageSupportRepository = languageSupportRepository; this.storageService = storageService; this.pluginsService = pluginsService; this.activeRulesService = activeRulesService; this.fileSystemService = fileSystemService; this.fileExclusionService = fileExclusionService; this.monitoringService = monitoringService; this.taskManager = taskManager; this.nodeJsService = nodeJsService; this.schedulerCache = schedulerCache; this.eventPublisher = eventPublisher; this.userAnalysisPropertiesRepository = clientAnalysisPropertiesRepository; this.openFilesRepository = openFilesRepository; this.automaticAnalysisEnabled = initializeParams.isAutomaticAnalysisEnabled(); this.clientFileSystemService = clientFileSystemService; this.esLintBridgeServerPath = initializeParams.getLanguageSpecificRequirements() != null && initializeParams.getLanguageSpecificRequirements().getJsTsRequirements() != null ? initializeParams.getLanguageSpecificRequirements().getJsTsRequirements().getBundlePath() : null; } @NotNull private static List getPatterns(Set enabledLanguages, Map analysisSettings) { List patterns = new ArrayList<>(); for (SonarLanguage language : enabledLanguages) { String propertyValue = analysisSettings.get(language.getFileSuffixesPropKey()); String[] extensions; if (propertyValue == null) { extensions = language.getDefaultFileSuffixes(); } else { extensions = MultivalueProperty.parseAsCsv(language.getFileSuffixesPropKey(), propertyValue); } for (String suffix : extensions) { var sanitizedExtension = sanitizeExtension(suffix); patterns.add("**/*." + sanitizedExtension); } } return patterns; } public List getSupportedFilePatterns(String configScopeId) { var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); Set enabledLanguages; Map analysisSettings; if (effectiveBinding.isEmpty()) { enabledLanguages = languageSupportRepository.getEnabledLanguagesInStandaloneMode(); analysisSettings = Collections.emptyMap(); } else { enabledLanguages = languageSupportRepository.getEnabledLanguagesInConnectedMode(); analysisSettings = storageService.binding(effectiveBinding.get()) .analyzerConfiguration().read().getSettings().getAll(); } // TODO merge client side analysis settings return getPatterns(enabledLanguages, analysisSettings); } private AnalysisConfiguration getAnalysisConfigForEngine(String configScopeId, Set filesUrisToAnalyze, Map extraProperties, boolean hotspotsOnly, TriggerType triggerType, Trace trace) { trace.setData("trigger", triggerType); var baseDir = startChild(trace, "getBaseDir", ANALYSIS_CFG_FOR_ENGINE, () -> fileSystemService.getBaseDir(configScopeId)); var filesToAnalyze = startChild(trace, "refineAnalysisScope", ANALYSIS_CFG_FOR_ENGINE, () -> fileExclusionService.filterOutExcludedFiles(configScopeId, baseDir, filesUrisToAnalyze)); var actualBaseDir = baseDir == null ? findCommonPrefix(filesUrisToAnalyze) : baseDir; var analysisConfig = getAnalysisConfig(configScopeId, hotspotsOnly, trace); var analysisProperties = analysisConfig.analysisProperties(); var inferredAnalysisProperties = startChild(trace, "getInferredAnalysisProperties", ANALYSIS_CFG_FOR_ENGINE, () -> client.getInferredAnalysisProperties(new GetInferredAnalysisPropertiesParams( configScopeId, filesToAnalyze.stream().map(ClientFile::getUri).toList())).join().getProperties()); analysisProperties.putAll(inferredAnalysisProperties); trace.setData("activeRulesCount", analysisConfig.activeRules().size()); return startChild(trace, "buildAnalysisConfiguration", ANALYSIS_CFG_FOR_ENGINE, () -> AnalysisConfiguration.builder() .addInputFiles(filesToAnalyze.stream().map(BackendInputFile::new).toList()) .putAllExtraProperties(analysisProperties) // properties sent by client using new API were merged above // but this line is important for backward compatibility for clients directly triggering analysis .putAllExtraProperties(extraProperties) .addActiveRules(analysisConfig.activeRules()) .setBaseDir(actualBaseDir) .build()); } private AnalysisConfig getAnalysisConfig(String configScopeId, boolean hotspotsOnly, @Nullable Trace trace) { var bindingOpt = configurationRepository.getEffectiveBinding(configScopeId); var userAnalysisProperties = userAnalysisPropertiesRepository.getUserProperties(configScopeId); // If the client (IDE) has specified a bundle path, use it if (this.esLintBridgeServerPath != null) { userAnalysisProperties.put(SONAR_INTERNAL_BUNDLE_PATH_ANALYSIS_PROP, this.esLintBridgeServerPath.toString()); } if (trace != null) { trace.setData("connected", bindingOpt.isPresent()); } if (bindingOpt.isPresent()) { var binding = bindingOpt.get(); var analyzerConfig = storageService.binding(binding).analyzerConfiguration(); if (analyzerConfig.isValid()) { return getConnectedAnalysisConfig(binding, hotspotsOnly, userAnalysisProperties, trace); } else { // This can happen when a standalone analysis was scheduled and a synchronization happened in between. // The config scope is bound, but the config file is not yet created. // In this case, we still trigger the analysis as if it was in standalone instead of failing. // This log should not appear when a synchronization is not happening. LOG.warn("Could not retrieve connected analysis configuration, falling back to standalone configuration"); } } return getStandaloneAnalysisConfig(userAnalysisProperties, trace); } private AnalysisConfig getConnectedAnalysisConfig(Binding binding, boolean hotspotsOnly, Map userAnalysisProperties, @Nullable Trace trace) { var serverProperties = startChild(trace, "serverProperties", GET_ANALYSIS_CFG, () -> storageService.binding(binding).analyzerConfiguration().read().getSettings().getAll()); var analysisProperties = new HashMap<>(serverProperties); analysisProperties.putAll(userAnalysisProperties); var connectedActiveRules = startChild(trace, "buildConnectedActiveRules", GET_ANALYSIS_CFG, () -> { var activeRules = activeRulesService.getConnectedActiveRules(binding); return hotspotsOnly ? activeRules.stream().filter(activeRule -> activeRule.type() == RuleType.SECURITY_HOTSPOT).toList() : activeRules; }); return new AnalysisConfig(connectedActiveRules, analysisProperties); } private AnalysisConfig getStandaloneAnalysisConfig(Map userAnalysisProperties, @Nullable Trace trace) { var standaloneActiveRules = startChild(trace, "buildStandaloneActiveRules", GET_ANALYSIS_CFG, activeRulesService::getStandaloneActiveRules); return new AnalysisConfig(standaloneActiveRules, userAnalysisProperties); } private static Path findCommonPrefix(Set uris) { var paths = uris.stream().map(Paths::get).toList(); Path currentPrefixCandidate = paths.get(0).getParent(); while (currentPrefixCandidate.getNameCount() > 0 && !isPrefixForAll(currentPrefixCandidate, paths)) { currentPrefixCandidate = currentPrefixCandidate.getParent(); } return currentPrefixCandidate; } private static boolean isPrefixForAll(Path prefixCandidate, Collection paths) { return paths.stream().allMatch(p -> p.startsWith(prefixCandidate)); } public void setUserAnalysisProperties(String configScopeId, Map properties) { if (userAnalysisPropertiesRepository.setUserProperties(configScopeId, properties)) { autoAnalyzeOpenFiles(configScopeId); } } public void didChangePathToCompileCommands(String configScopeId, @Nullable String pathToCompileCommands) { if (userAnalysisPropertiesRepository.setOrUpdatePathToCompileCommands(configScopeId, pathToCompileCommands)) { autoAnalyzeOpenFiles(configScopeId); } } @EventListener public void onPluginsSynchronized(PluginsSynchronizedEvent event) { var connectionId = event.connectionId(); if (connectionId != null) { schedulerCache.reloadPlugins(connectionId); checkIfReadyForAnalysis(configurationRepository.getBoundScopesToConnection(connectionId) .stream().map(BoundScope::getConfigScopeId).collect(Collectors.toSet())); } else { // On-demand plugins are application-wide and used as fallback in connected mode schedulerCache.reloadStandalonePlugins(); checkIfReadyForAnalysis(new HashSet<>(analysisReadinessByConfigScopeId.keySet())); } } @EventListener public void onConfigurationScopeAdded(ConfigurationScopesAddedWithBindingEvent event) { var configScopeIds = event.getConfigScopeIds(); checkIfReadyForAnalysis(configScopeIds); } @EventListener public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) { var removedConfigurationScopeId = event.getRemovedConfigurationScopeId(); analysisReadinessByConfigScopeId.remove(removedConfigurationScopeId); client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(Set.of(removedConfigurationScopeId), false)); schedulerCache.unregisterModule(removedConfigurationScopeId, event.removedBindingConfiguration().connectionId()); } @EventListener public void onBindingConfigurationChanged(BindingConfigChangedEvent event) { var configScopeId = event.configScopeId(); checkIfReadyForAnalysis(Set.of(configScopeId)); } @EventListener public void onAnalyzerConfigurationSynchronized(AnalyzerConfigurationSynchronized event) { checkIfReadyForAnalysis(event.configScopeIds()); } @EventListener public void onConfigurationScopesSynchronized(ConfigurationScopesSynchronizedEvent event) { checkIfReadyForAnalysis(event.getConfigScopeIds()); } @EventListener public void onPluginStatusUpdateEvent(PluginStatusUpdateEvent event) { if (event.newStatuses().stream().anyMatch(s -> s.state() == ArtifactState.ACTIVE || s.state() == ArtifactState.SYNCED || s.state() == ArtifactState.FAILED)) { var connectionId = event.connectionId(); Set configScopeIds; if (connectionId == null) { // On-demand plugins are application-wide and used as fallback in connected mode, so re-check all scopes configScopeIds = new HashSet<>(analysisReadinessByConfigScopeId.keySet()); } else { configScopeIds = configurationRepository.getBoundScopesToConnection(connectionId) .stream().map(BoundScope::getConfigScopeId).collect(Collectors.toSet()); } checkIfReadyForAnalysis(configScopeIds); } } @EventListener public void onFileSystemUpdated(FileSystemUpdatedEvent event) { sendModuleEvents(event.getAdded(), ModuleFileEvent.Type.CREATED); sendModuleEvents(event.getUpdated(), ModuleFileEvent.Type.MODIFIED); sendModuleEvents(event.getRemoved(), ModuleFileEvent.Type.DELETED); var updatedFileUrisByConfigScope = event.getUpdated().stream().collect(groupingBy(ClientFile::getConfigScopeId, mapping(ClientFile::getUri, toSet()))); updatedFileUrisByConfigScope.forEach((configScopeId, fileUris) -> { var openFileUris = openFilesRepository.getOpenFilesAmong(configScopeId, fileUris); scheduleAutomaticAnalysis(configScopeId, openFileUris); }); } @EventListener public void onFileOpened(FileOpenedEvent event) { scheduleAutomaticAnalysis(event.configurationScopeId(), Set.of(event.fileUri())); } @EventListener public void onStandaloneRulesConfigurationChanged(StandaloneRulesConfigurationChanged event) { if (!event.isOnlyDeactivated()) { // trigger an analysis if any rule was enabled reanalyseOpenFiles(this::isStandalone); } } @CheckForNull public UUID forceAnalyzeOpenFiles(String configScopeId) { var openFiles = openFilesRepository.getOpenFilesForConfigScope(configScopeId); if (openFiles.isEmpty()) { // we return UUID because one of the callers is RPC client, it should not call it for empty list of files return null; } return scheduleForcedAnalysis(configScopeId, openFiles, false); } public void autoAnalyzeOpenFiles(String configScopeId) { var openFiles = openFilesRepository.getOpenFilesForConfigScope(configScopeId); scheduleAutomaticAnalysis(configScopeId, openFiles); } @EventListener public void onServerActiveRulesChanged(ServerActiveRulesChanged event) { var activatedRules = event.activatedRules(); if (!activatedRules.isEmpty()) { reanalyseOpenFiles(not(this::isStandalone)); } } private boolean isStandalone(String configScopeId) { return configurationRepository.getEffectiveBinding(configScopeId).isEmpty(); } private void sendModuleEvents(List filesToProcess, ModuleFileEvent.Type type) { var filesByScopeId = filesToProcess.stream().collect(groupingBy(ClientFile::getConfigScopeId)); filesByScopeId.forEach((scopeId, files) -> { var scheduler = schedulerCache.getAnalysisSchedulerIfStarted(scopeId); if (scheduler != null) { files.forEach(file -> scheduler.post(new NotifyModuleEventCommand(scopeId, ClientModuleFileEvent.of(new BackendInputFile(file), type)))); } }); } public boolean shouldUseEnterpriseCSharpAnalyzer(String configurationScopeId) { var binding = configurationRepository.getEffectiveBinding(configurationScopeId); if (binding.isEmpty()) { return false; } else { var connectionId = binding.get().connectionId(); return pluginsService.shouldUseEnterpriseCSharpAnalyzer(connectionId); } } private void streamIssue(String configScopeId, UUID analysisId, List rawIssues, Issue issue) { var rawIssue = new RawIssue(issue); rawIssues.add(rawIssue); if (rawIssue.getRuleKey().contains("secrets")) { client.didDetectSecret(new DidDetectSecretParams(configScopeId)); } eventPublisher.publishEvent(new RawIssueDetectedEvent(configScopeId, analysisId, rawIssue)); } private void checkIfReadyForAnalysis(Set configurationScopeIds) { var readyConfigScopeIds = new HashSet(); var scopeThatBecameReady = new HashSet(); var scopeThatBecameNotReady = new HashSet(); configurationScopeIds.forEach(configScopeId -> { var readyForAnalysis = isReadyForAnalysis(configScopeId); var childrenScopesWithSameReadiness = configurationRepository.getChildrenWithInheritedBinding(configScopeId); var wasReady = analysisReadinessByConfigScopeId.put(configScopeId, readyForAnalysis); analysisReadinessByConfigScopeId.putAll(childrenScopesWithSameReadiness.stream().collect(toMap(Function.identity(), k -> readyForAnalysis))); if (readyForAnalysis && !Boolean.TRUE.equals(wasReady)) { scopeThatBecameReady.add(configScopeId); scopeThatBecameReady.addAll(childrenScopesWithSameReadiness); } else if (!readyForAnalysis && Boolean.TRUE.equals(wasReady)) { scopeThatBecameNotReady.add(configScopeId); scopeThatBecameNotReady.addAll(childrenScopesWithSameReadiness); } if (readyForAnalysis) { readyConfigScopeIds.add(configScopeId); readyConfigScopeIds.addAll(childrenScopesWithSameReadiness); } }); if (!scopeThatBecameReady.isEmpty()) { scopeThatBecameReady.forEach(scopeId -> { var scheduler = schedulerCache.getOrCreateAnalysisScheduler(scopeId); if (scheduler != null) { scheduler.wakeUp(); } }); client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(scopeThatBecameReady, true)); } if (!scopeThatBecameNotReady.isEmpty()) { client.didChangeAnalysisReadiness(new DidChangeAnalysisReadinessParams(scopeThatBecameNotReady, false)); } reanalyseOpenFiles(readyConfigScopeIds::contains); } private boolean isReadyForAnalysis(String configScopeId) { return configurationRepository.getEffectiveBinding(configScopeId) .map(this::isReadyForAnalysis) // standalone mode .orElse(true); } private boolean isReadyForAnalysis(Binding binding) { var bindingStorage = storageService.binding(binding); var analyzerConfigValid = bindingStorage.analyzerConfiguration().isValid(); var findingsStorageValid = bindingStorage.findings().wasEverUpdated(); var isReady = analyzerConfigValid // this is not strictly for analysis but for tracking && findingsStorageValid; LOG.debug("isReadyForAnalysis(connectionId: {}, sonarProjectKey: {}, plugins: {}, analyzer config: {}, findings: {}) => {}", binding.connectionId(), binding.sonarProjectKey(), true, analyzerConfigValid, findingsStorageValid, isReady); return isReady; } public InstalledNodeJs getAutoDetectedNodeJs() { return nodeJsService.getAutoDetectedNodeJs(); } public void didChangeAutomaticAnalysisSetting(boolean enabled) { var previouslyEnabled = this.automaticAnalysisEnabled; this.automaticAnalysisEnabled = enabled; if (previouslyEnabled != enabled) { LOG.debug("Automatic analysis setting changed to: {}", enabled); eventPublisher.publishEvent(new AutomaticAnalysisSettingChangedEvent(enabled)); if (enabled) { triggerAnalysisForOpenFiles(); } } } public UUID analyzeFullProject(String configScopeId, boolean hotspotsOnly) { var files = clientFileSystemService.getFiles(configScopeId); return scheduleForcedAnalysis(configScopeId, files.stream().map(ClientFile::getUri).collect(toSet()), hotspotsOnly); } public UUID analyzeFileList(String configScopeId, List filesToAnalyze) { return scheduleForcedAnalysis(configScopeId, Set.copyOf(filesToAnalyze), false); } public UUID analyzeVCSChangedFiles(String configScopeId) { var changedFiles = getVCSChangedFiles(clientFileSystemService.getBaseDir(configScopeId)); return scheduleForcedAnalysis(configScopeId, changedFiles, false); } private void triggerAnalysisForOpenFiles() { openFilesRepository.getOpenFilesByConfigScopeId() .forEach((configurationScopeId, files) -> scheduleForcedAnalysis(configurationScopeId, files, false)); } private UUID scheduleForcedAnalysis(String configurationScopeId, Set files, boolean hotspotsOnly) { var analysisId = UUID.randomUUID(); var rawIssues = new ArrayList(); schedule(configurationScopeId, getAnalyzeCommand(configurationScopeId, files, rawIssues, hotspotsOnly, TriggerType.FORCED, analysisId), analysisId, rawIssues, true, null) .exceptionally(e -> { if (!(e instanceof CancellationException)) { LOG.error("Error during analysis", e); } return null; }); return analysisId; } public CompletableFuture scheduleAnalysis(String configurationScopeId, UUID analysisId, Set files, Map extraProperties, boolean shouldFetchServerIssues, TriggerType triggerType, SonarLintCancelMonitor cancelChecker) { var rawIssues = new ArrayList(); var trace = newAnalysisTrace(); var analysisTask = new AnalyzeCommand(configurationScopeId, analysisId, triggerType, () -> getAnalysisConfigForEngine(configurationScopeId, files, extraProperties, false, triggerType, trace), issue -> streamIssue(configurationScopeId, analysisId, rawIssues, issue), trace, cancelChecker, taskManager, inputFiles -> analysisStarted(configurationScopeId, analysisId, inputFiles), () -> analysisReadinessByConfigScopeId.getOrDefault(configurationScopeId, false), files, extraProperties); return schedule(configurationScopeId, analysisTask, analysisId, rawIssues, shouldFetchServerIssues, trace); } private Trace newAnalysisTrace() { var newTrace = monitoringService.newTrace("AnalysisService", "analyze"); var currentRuntime = Runtime.getRuntime(); newTrace.setData("availableProcessors", currentRuntime.availableProcessors()); newTrace.setData("totalMemory", currentRuntime.totalMemory()); newTrace.setData("maxMemory", currentRuntime.maxMemory()); return newTrace; } private void scheduleAutomaticAnalysis(String configScopeId, Set filesToAnalyze) { if (automaticAnalysisEnabled && !filesToAnalyze.isEmpty()) { var rawIssues = new ArrayList(); var analysisId = UUID.randomUUID(); var command = getAnalyzeCommand(configScopeId, filesToAnalyze, rawIssues, false, TriggerType.AUTO, analysisId); schedule(configScopeId, command, analysisId, rawIssues, true, null) .exceptionally(exception -> { if (!(exception instanceof CancellationException) && !(exception instanceof CompletionException && exception.getCause() instanceof CancellationException)) { LOG.error("Error during automatic analysis", exception); } return null; }); } } private void analysisStarted(String configurationScopeId, UUID analysisId, List inputFiles) { eventPublisher.publishEvent(new AnalysisStartedEvent(configurationScopeId, analysisId, inputFiles)); } private CompletableFuture schedule(String configScopeId, AnalyzeCommand command, UUID analysisId, ArrayList rawIssues, boolean shouldFetchServerIssues, @Nullable Trace trace) { var scheduler = startChild(trace, "getOrCreateAnalysisScheduler", "schedule", () -> schedulerCache.getOrCreateAnalysisScheduler(configScopeId, command.getTrace())); // Plugins may have become ready during scheduler creation (e.g. on-demand cache hit); re-check readiness so the scheduler is woken if needed if (BooleanUtils.isNotTrue(analysisReadinessByConfigScopeId.get(configScopeId))) { checkIfReadyForAnalysis(Set.of(configScopeId)); } startChild(trace, "post", "schedule", () -> scheduler.post(command)); var result = command.getFutureResult(); result.exceptionally(exception -> { eventPublisher.publishEvent(new AnalysisFailedEvent(analysisId)); if (exception instanceof CancellationException) { LOG.debug("Analysis canceled"); } else { LOG.error("Error during analysis", exception); } return null; }); return result .thenApply(analysisResults -> { var languagePerFile = analysisResults.languagePerFile().entrySet().stream().collect(HashMap::new, (map, entry) -> map.put(entry.getKey().uri(), entry.getValue()), HashMap::putAll); logSummary(rawIssues, analysisResults.getDuration()); eventPublisher.publishEvent(new AnalysisFinishedEvent(analysisId, configScopeId, analysisResults.getDuration(), languagePerFile, analysisResults.failedAnalysisFiles().isEmpty(), rawIssues, shouldFetchServerIssues)); return new AnalysisResult( analysisResults.failedAnalysisFiles().stream().map(ClientInputFile::getClientObject).map(clientObj -> ((ClientFile) clientObj).getUri()).collect(Collectors.toSet()), rawIssues); }); } private AnalyzeCommand getAnalyzeCommand(String configurationScopeId, Set files, ArrayList rawIssues, boolean hotspotsOnly, TriggerType triggerType, UUID analysisId) { var trace = newAnalysisTrace(); return new AnalyzeCommand(configurationScopeId, analysisId, triggerType, () -> getAnalysisConfigForEngine(configurationScopeId, files, Map.of(), hotspotsOnly, triggerType, trace), issue -> streamIssue(configurationScopeId, analysisId, rawIssues, issue), trace, new SonarLintCancelMonitor(), taskManager, inputFiles -> analysisStarted(configurationScopeId, analysisId, inputFiles), () -> analysisReadinessByConfigScopeId.getOrDefault(configurationScopeId, false), files, Map.of()); } private void reanalyseOpenFiles(Predicate configScopeFilter) { openFilesRepository.getOpenFilesByConfigScopeId() .entrySet() .stream().filter(entry -> configScopeFilter.test(entry.getKey())) .forEach(entry -> scheduleAutomaticAnalysis(entry.getKey(), entry.getValue())); } private static void logSummary(List rawIssues, Duration analysisDuration) { // ignore project-level issues for now var fileRawIssues = rawIssues.stream().filter(issue -> issue.getTextRange() != null).toList(); var issuesCount = fileRawIssues.stream().filter(not(RawIssue::isSecurityHotspot)).count(); var hotspotsCount = fileRawIssues.stream().filter(RawIssue::isSecurityHotspot).count(); LOG.info("Analysis detected {} and {} in {}ms", pluralize(issuesCount, "issue"), pluralize(hotspotsCount, "Security Hotspot"), analysisDuration.toMillis()); } private record AnalysisConfig(List activeRules, Map analysisProperties) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisStartedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.function.UnaryOperator; import java.util.stream.StreamSupport; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import static java.util.stream.Collectors.toSet; public class AnalysisStartedEvent { private final String configurationScopeId; private final UUID analysisId; private final List files; public AnalysisStartedEvent(String configurationScopeId, UUID analysisId, Iterable files) { this.configurationScopeId = configurationScopeId; this.analysisId = analysisId; this.files = StreamSupport.stream(files.spliterator(), false).toList(); } public UUID getAnalysisId() { return analysisId; } public String getConfigurationScopeId() { return configurationScopeId; } public Set getFileRelativePaths() { return files.stream().map(ClientInputFile::relativePath).map(Path::of).collect(toSet()); } public Set getFileUris() { return files.stream().map(ClientInputFile::uri).collect(toSet()); } public UnaryOperator getFileContentProvider() { return path -> files.stream() .filter(ClientInputFile::isDirty) .filter(clientInputFile -> clientInputFile.relativePath().equals(path)) .findFirst() .map(AnalysisStartedEvent::getClientInputFileContent) .orElse(null); } private static String getClientInputFileContent(ClientInputFile clientInputFile) { try { return clientInputFile.contents(); } catch (IOException e) { return ""; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/AutomaticAnalysisSettingChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; public record AutomaticAnalysisSettingChangedEvent(boolean isAutomaticAnalysisEnabled) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendInputFile.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.util.FileUtils; import org.sonarsource.sonarlint.core.fs.ClientFile; public class BackendInputFile implements ClientInputFile { private final ClientFile clientFile; public BackendInputFile(ClientFile clientFile) { this.clientFile = clientFile; } @Override public String getPath() { return FileUtils.getFilePathFromUri(clientFile.getUri()).toAbsolutePath().toString(); } @Override public boolean isTest() { return clientFile.isTest(); } @Override public Charset getCharset() { return clientFile.getCharset(); } @Override public ClientFile getClientObject() { return clientFile; } @Override public InputStream inputStream() throws IOException { return clientFile.inputStream(); } @Override public String contents() { return clientFile.getContent(); } @Override public String relativePath() { return clientFile.getClientRelativePath().toString(); } @Override public URI uri() { return clientFile.getUri(); } @Override public SonarLanguage language() { return clientFile.getDetectedLanguage(); } @Override public boolean isDirty() { return clientFile.isDirty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendModuleFileSystem.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.stream.Stream; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientModuleFileSystem; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; public class BackendModuleFileSystem implements ClientModuleFileSystem { private final ClientFileSystemService clientFileSystemService; private final String configScopeId; public BackendModuleFileSystem(ClientFileSystemService clientFileSystemService, String configScopeId) { this.clientFileSystemService = clientFileSystemService; this.configScopeId = configScopeId; } @Override public Stream files(String suffix, InputFile.Type type) { return files() .filter(file -> file.relativePath().endsWith(suffix)) .filter(file -> file.isTest() == (type == InputFile.Type.TEST)); } @Override public Stream files() { return this.clientFileSystemService.getFiles(configScopeId).stream().map(BackendInputFile::new); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/ClientNodeJsPathChanged.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; public class ClientNodeJsPathChanged { ClientNodeJsPathChanged() { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/IssuesRaisedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.List; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; public record IssuesRaisedEvent(List issues) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/NodeJsService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.nio.file.Path; import java.util.Collections; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.SystemUtils; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.nodejs.InstalledNodeJs; import org.sonarsource.sonarlint.core.nodejs.NodeJsHelper; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.springframework.context.ApplicationEventPublisher; /** * Keep track of the Node.js executable to be used by analysis */ public class NodeJsService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ApplicationEventPublisher eventPublisher; private final boolean isNodeJsNeeded; private volatile boolean nodeAutoDetected; @Nullable private InstalledNodeJs autoDetectedNodeJs; @Nullable private Path clientNodeJsPath; private boolean clientForcedNodeJsDetected; @Nullable private InstalledNodeJs clientForcedNodeJs; public NodeJsService(InitializeParams initializeParams, ApplicationEventPublisher eventPublisher) { var languageSpecificRequirements = initializeParams.getLanguageSpecificRequirements(); this.clientNodeJsPath = languageSpecificRequirements == null || languageSpecificRequirements.getJsTsRequirements() == null ? null : languageSpecificRequirements.getJsTsRequirements().getClientNodeJsPath(); this.isNodeJsNeeded = isNodeJsNeeded(initializeParams); this.eventPublisher = eventPublisher; } private static boolean isNodeJsNeeded(InitializeParams initializeParams) { // in theory all clients bundle SonarJS, so this should always return true // in practice and to speed up tests, we will avoid looking up Node.js if SonarJS is not present var languagesNeedingNodeJsInSonarJs = SonarPlugin.JS.getLanguages().stream().map(l -> Language.valueOf(l.name())).collect(Collectors.toSet()); return !Collections.disjoint(initializeParams.getEnabledLanguagesInStandaloneMode(), languagesNeedingNodeJsInSonarJs) || !Collections.disjoint(initializeParams.getExtraEnabledLanguagesInConnectedMode(), languagesNeedingNodeJsInSonarJs); } @CheckForNull public synchronized InstalledNodeJs didChangeClientNodeJsPath(@Nullable Path clientNodeJsPath) { if (!Objects.equals(this.clientNodeJsPath, clientNodeJsPath)) { this.clientNodeJsPath = clientNodeJsPath; this.clientForcedNodeJsDetected = false; this.eventPublisher.publishEvent(new ClientNodeJsPathChanged()); } var forcedNodeJs = getClientForcedNodeJs(); return forcedNodeJs == null ? null : new InstalledNodeJs(forcedNodeJs.getPath(), forcedNodeJs.getVersion()); } @CheckForNull public synchronized InstalledNodeJs getActiveNodeJs() { return clientNodeJsPath == null ? getAutoDetectedNodeJs() : getClientForcedNodeJs(); } public synchronized Optional getActiveNodeJsVersion() { return Optional.ofNullable(getActiveNodeJs()).map(InstalledNodeJs::getVersion); } @CheckForNull public InstalledNodeJs getAutoDetectedNodeJs() { if (!nodeAutoDetected) { if (!isNodeJsNeeded) { LOG.debug("Skip Node.js auto-detection as no plugins require it"); nodeAutoDetected = true; return null; } var helper = new NodeJsHelper(); autoDetectedNodeJs = helper.autoDetect(); nodeAutoDetected = true; logAutoDetectionResults(autoDetectedNodeJs); } return autoDetectedNodeJs; } @CheckForNull private InstalledNodeJs getClientForcedNodeJs() { if (!clientForcedNodeJsDetected) { var helper = new NodeJsHelper(); clientForcedNodeJs = helper.detect(clientNodeJsPath); clientForcedNodeJsDetected = true; logClientForcedDetectionResults(clientForcedNodeJs); } return clientForcedNodeJs; } private static void logAutoDetectionResults(@Nullable InstalledNodeJs autoDetectedNode) { if (autoDetectedNode != null) { LOG.debug("Auto-detected Node.js path set to: {} (version {})", autoDetectedNode.getPath(), autoDetectedNode.getVersion()); } else { LOG.warn( "Node.js could not be automatically detected, has to be configured manually in the SonarLint preferences!"); if (SystemUtils.IS_OS_MAC_OSX) { // In case of macOS or could not be found, just add the warning for the user and us if we have to provide // support on that matter at some point. LOG.warn( "Automatic detection does not work on macOS when added to PATH from user shell configuration (e.g. Bash)"); } } } private static void logClientForcedDetectionResults(@Nullable InstalledNodeJs detectedNode) { if (detectedNode != null) { LOG.debug("Node.js path set to: {} (version {})", detectedNode.getPath(), detectedNode.getVersion()); } else { LOG.warn( "Configured Node.js could not be detected, please check your configuration in the SonarLint settings"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssue.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.net.URI; import java.nio.file.Path; import java.util.Collection; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.IntStream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.active.rules.ActiveRuleDetails; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.Flow; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.api.QuickFix; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.tracking.TextRangeUtils; public class RawIssue { private final Issue issue; private final ActiveRuleDetails activeRule; private final Map impacts = new EnumMap<>(SoftwareQuality.class); @Nullable private final String textRangeHash; @Nullable private final String lineHash; public RawIssue(Issue issue) { this.issue = issue; this.activeRule = (ActiveRuleDetails) issue.getActiveRule(); this.impacts.putAll(activeRule.impacts()); this.impacts.putAll(issue.getOverriddenImpacts()); var textRangeWithHash = TextRangeUtils.getTextRangeWithHash(getTextRange(), getClientInputFile()); this.textRangeHash = textRangeWithHash == null ? null : textRangeWithHash.getHash(); var lineWithHash = TextRangeUtils.getLineWithHash(issue.getTextRange(), getClientInputFile()); this.lineHash = lineWithHash == null ? null : lineWithHash.getHash(); } public IssueSeverity getSeverity() { return activeRule.issueSeverity(); } public RuleType getRuleType() { return activeRule.type(); } public boolean isSecurityHotspot() { return getRuleType() == RuleType.SECURITY_HOTSPOT; } public CleanCodeAttribute getCleanCodeAttribute() { return activeRule.cleanCodeAttribute(); } public Map getImpacts() { return impacts; } public String getRuleKey() { return activeRule.ruleKeyString(); } public String getMessage() { return issue.getMessage(); } public List getFlows() { return issue.flows(); } public List getQuickFixes() { return issue.quickFixes(); } @CheckForNull public TextRange getTextRange() { return issue.getTextRange(); } @CheckForNull public Path getIdeRelativePath() { var inputFile = issue.getInputFile(); return inputFile != null ? Path.of(inputFile.relativePath()) : null; } @CheckForNull public URI getFileUri() { var inputFile = issue.getInputFile(); return inputFile != null ? inputFile.uri() : null; } public boolean isInFile() { return issue.getInputFile() != null; } @CheckForNull public ClientInputFile getClientInputFile() { return issue.getInputFile(); } @CheckForNull public VulnerabilityProbability getVulnerabilityProbability() { return activeRule.vulnerabilityProbability(); } @CheckForNull public String getRuleDescriptionContextKey() { return issue.getRuleDescriptionContextKey().orElse(null); } public Collection getLineNumbers() { Set lineNumbers = new HashSet<>(); Optional.ofNullable(getTextRange()) .map(textRange -> IntStream.rangeClosed(textRange.getStartLine(), textRange.getEndLine())) .ifPresent(intStream -> intStream.forEach(lineNumbers::add)); getFlows() .forEach(flow -> flow.locations().stream() .filter(issueLocation -> Objects.nonNull(issueLocation.getStartLine())) .filter(issueLocation -> Objects.nonNull(issueLocation.getEndLine())) .map(issueLocation -> IntStream.rangeClosed(issueLocation.getStartLine(), issueLocation.getEndLine())) .forEach(intStream -> intStream.forEach(lineNumbers::add))); return lineNumbers; } public Optional getLine() { return Optional.ofNullable(issue.getStartLine()); } public Optional getTextRangeHash() { return Optional.ofNullable(textRangeHash); } public Optional getLineHash() { return Optional.ofNullable(lineHash); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssueDetectedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.UUID; public record RawIssueDetectedEvent(String configurationScopeId, UUID analysisId, RawIssue detectedIssue) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/UserAnalysisPropertiesRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; public class UserAnalysisPropertiesRepository { private static final String PATH_TO_COMPILE_COMMANDS_ANALYZER_PROPERTY = "sonar.cfamily.compile-commands"; private final Map pathToCompileCommandsByConfigScope = new ConcurrentHashMap<>(); private final Map> propertiesByConfigScope = new ConcurrentHashMap<>(); public Map getUserProperties(String configurationScopeId) { var properties = propertiesByConfigScope.getOrDefault(configurationScopeId, new HashMap<>()); var pathToCompileCommands = pathToCompileCommandsByConfigScope.get(configurationScopeId); if (pathToCompileCommands == null) { return properties; } properties.put(PATH_TO_COMPILE_COMMANDS_ANALYZER_PROPERTY, pathToCompileCommands); return properties; } public boolean setUserProperties(String configurationScopeId, Map userProperties) { var oldProperties = propertiesByConfigScope.get(configurationScopeId); var newProperties = new HashMap<>(userProperties); var changed = !newProperties.equals(oldProperties); if (changed) { propertiesByConfigScope.put(configurationScopeId, newProperties); } return changed; } public boolean setOrUpdatePathToCompileCommands(String configurationScopeId, @Nullable String value) { var newValue = value == null ? "" : value; var oldValue = pathToCompileCommandsByConfigScope.get(configurationScopeId); var changed = !newValue.equals(oldValue); if (changed) { pathToCompileCommandsByConfigScope.put(configurationScopeId, newValue); } return changed; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.analysis; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/branch/MatchedSonarProjectBranchChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.branch; public class MatchedSonarProjectBranchChangedEvent { private final String configurationScopeId; private final String newBranchName; public MatchedSonarProjectBranchChangedEvent(String configurationScopeId, String newBranchName) { this.configurationScopeId = configurationScopeId; this.newBranchName = newBranchName; } public String getConfigurationScopeId() { return configurationScopeId; } public String getNewBranchName() { return newBranchName; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/branch/SonarProjectBranchMatchingEndedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.branch; public class SonarProjectBranchMatchingEndedEvent { private final String configurationScopeId; public SonarProjectBranchMatchingEndedEvent(String configurationScopeId) { this.configurationScopeId = configurationScopeId; } public String getConfigurationScopeId() { return configurationScopeId; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/branch/SonarProjectBranchTrackingService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.branch; import jakarta.annotation.PreDestroy; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CancellationException; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.slf4j.MDC; import org.sonarsource.sonarlint.core.commons.SmartCancelableLoadingCache; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.DidChangeMatchedSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchSonarProjectBranchParams; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesChangedEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; /** * This service keep track of the currently matched Sonar project branch for each configuration scope. */ public class SonarProjectBranchTrackingService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final StorageService storageService; private final ConfigurationRepository configurationRepository; private final ApplicationEventPublisher applicationEventPublisher; private final SmartCancelableLoadingCache cachedMatchingBranchByConfigScope = new SmartCancelableLoadingCache<>("sonarlint-branch-matcher", this::matchSonarProjectBranch, this::afterCachedValueRefreshed); public SonarProjectBranchTrackingService(SonarLintRpcClient client, StorageService storageService, ConfigurationRepository configurationRepository, ApplicationEventPublisher applicationEventPublisher) { this.client = client; this.storageService = storageService; this.configurationRepository = configurationRepository; this.applicationEventPublisher = applicationEventPublisher; } public Optional awaitEffectiveSonarProjectBranch(String configurationScopeId) { return Optional.ofNullable(cachedMatchingBranchByConfigScope.get(configurationScopeId)); } private void afterCachedValueRefreshed(String configScopeId, @Nullable String oldValue, @Nullable String newValue) { if (!Objects.equals(newValue, oldValue)) { LOG.debug("Matched Sonar project branch for configuration scope '{}' changed from '{}' to '{}'", configScopeId, oldValue, newValue); if (newValue != null) { client.didChangeMatchedSonarProjectBranch(new DidChangeMatchedSonarProjectBranchParams(configScopeId, newValue)); applicationEventPublisher.publishEvent(new MatchedSonarProjectBranchChangedEvent(configScopeId, newValue)); } } else { LOG.debug("Matched Sonar project branch for configuration scope '{}' is still '{}'", configScopeId, newValue); } } @EventListener public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) { var removedConfigScopeId = event.getRemovedConfigurationScopeId(); LOG.debug("Configuration scope '{}' removed, clearing matched branch", removedConfigScopeId); cachedMatchingBranchByConfigScope.clear(removedConfigScopeId); } @EventListener public void onConfigurationScopesAdded(ConfigurationScopesAddedWithBindingEvent event) { var configScopeIds = event.getConfigScopeIds(); configScopeIds.forEach(configScopeId -> { var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isPresent()) { var branchesStorage = storageService.binding(effectiveBinding.get()).branches(); if (branchesStorage.exists()) { LOG.debug("Bound configuration scope '{}' added with an existing storage, queuing matching of the Sonar project branch...", configScopeId); cachedMatchingBranchByConfigScope.refreshAsync(configScopeId); } } }); } @EventListener public void onBindingChanged(BindingConfigChangedEvent bindingChanged) { var configScopeId = bindingChanged.configScopeId(); if (!bindingChanged.newConfig().isBound()) { LOG.debug("Configuration scope '{}' unbound, clearing matched branch", configScopeId); cachedMatchingBranchByConfigScope.clear(configScopeId); } else { LOG.debug("Configuration scope '{}' binding changed, queuing matching of the Sonar project branch...", configScopeId); cachedMatchingBranchByConfigScope.refreshAsync(configScopeId); } } @EventListener public void onSonarProjectBranchChanged(SonarProjectBranchesChangedEvent event) { var configScopeIds = configurationRepository.getBoundScopesToConnectionAndSonarProject(event.getConnectionId(), event.getSonarProjectKey()); configScopeIds.forEach(boundScope -> { LOG.debug("Sonar project branch changed for configuration scope '{}', queuing matching of the Sonar project branch...", boundScope.getConfigScopeId()); cachedMatchingBranchByConfigScope.refreshAsync(boundScope.getConfigScopeId()); }); } public void didVcsRepositoryChange(String configScopeId) { LOG.debug("VCS repository changed for configuration scope '{}', queuing matching of the Sonar project branch...", configScopeId); cachedMatchingBranchByConfigScope.refreshAsync(configScopeId); } private String matchSonarProjectBranch(String configurationScopeId, SonarLintCancelMonitor cancelMonitor) { MDC.put("configScopeId", configurationScopeId); LOG.debug("Matching Sonar project branch"); var effectiveBindingOpt = configurationRepository.getEffectiveBinding(configurationScopeId); if (effectiveBindingOpt.isEmpty()) { LOG.debug("No binding for configuration scope"); return null; } var effectiveBinding = effectiveBindingOpt.get(); var branchesStorage = storageService.binding(effectiveBinding).branches(); if (!branchesStorage.exists()) { LOG.info("Cannot match Sonar branch, storage is empty"); return null; } var storedBranches = branchesStorage.read(); var mainBranchName = storedBranches.getMainBranchName(); var matchedSonarBranch = requestClientToMatchSonarProjectBranch(configurationScopeId, mainBranchName, storedBranches.getBranchNames(), cancelMonitor); if (matchedSonarBranch == null) { matchedSonarBranch = mainBranchName; } cancelMonitor.checkCanceled(); return matchedSonarBranch; } @CheckForNull private String requestClientToMatchSonarProjectBranch(String configurationScopeId, String mainSonarBranchName, Set allSonarBranchesNames, SonarLintCancelMonitor cancelMonitor) { var matchSonarProjectBranchResponseCompletableFuture = client .matchSonarProjectBranch(new MatchSonarProjectBranchParams(configurationScopeId, mainSonarBranchName, allSonarBranchesNames)); cancelMonitor.onCancel(() -> matchSonarProjectBranchResponseCompletableFuture.cancel(true)); try { return matchSonarProjectBranchResponseCompletableFuture.join().getMatchedSonarProjectBranch(); } catch (CancellationException e) { throw e; } catch (Exception e) { LOG.debug("Error while matching Sonar project branch for configuration scope '{}'", configurationScopeId, e); return null; } } @PreDestroy public void shutdown() { cachedMatchingBranchByConfigScope.close(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/branch/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.branch; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/Binding.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; public record Binding(String connectionId, String sonarProjectKey) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/BoundScope.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; /** * A configuration scope that is bound to a connection and a Sonar project. */ public class BoundScope { private final String configScopeId; private final Binding binding; public BoundScope(String configScopeId, String connectionId, String sonarProjectKey) { this(configScopeId, new Binding(connectionId, sonarProjectKey)); } public BoundScope(String configScopeId, Binding binding) { this.configScopeId = configScopeId; this.binding = binding; } public String getConfigScopeId() { return configScopeId; } public Binding getBinding() { return binding; } public String getConnectionId() { return binding.connectionId(); } public String getSonarProjectKey() { return binding.sonarProjectKey(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/DebounceComputer.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; /** * The goal of this class is to debounce calls to a function that computes a value. Multiple threads can be blocked on the {@link #get()} method, waiting for the end of the computation. * If a {@link #scheduleComputationAsync()} is called while a computation is in progress, an attempt will be made to cancel the current computation, and a new computation will be scheduled. * Last feature: it is possible to register a listener that will be notified only after a successful computation (not after a cancellation). */ class DebounceComputer { private final Function valueComputer; private final ExecutorServiceShutdownWatchable executorService; @Nullable private final Listener listener; private CompletableFuture valueFuture = new CompletableFuture<>(); @Nullable private CompletableFuture computeFuture; // The last computed value (a compute task went to completion without cancellation). Can be null if the compute task failed. @Nullable private V value; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public interface Listener { void afterComputedValueRefreshed(@Nullable V oldValue, @Nullable V newValue); } public DebounceComputer(Function valueComputer, ExecutorServiceShutdownWatchable executorService, @Nullable Listener listener) { this.valueComputer = valueComputer; this.executorService = executorService; this.listener = listener; } public V get() { return getValueFuture().join(); } public void scheduleComputationAsync() { lock.writeLock().lock(); try { if (computeFuture != null) { computeFuture.cancel(false); try { computeFuture.join(); } catch (Exception ignore) { // expected Cancellation exception, but we can ignore any other error since we are going to compute a new value anyway } computeFuture = null; } if (valueFuture.isDone()) { valueFuture = new CompletableFuture<>(); } var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); CompletableFuture newComputeFuture = CompletableFuture.supplyAsync(() -> { cancelMonitor.checkCanceled(); return valueComputer.apply(cancelMonitor); }, executorService); newComputeFuture.whenComplete((newValue, error) -> { if (error instanceof CancellationException) { cancelMonitor.cancel(); } }); newComputeFuture.whenComplete(this::whenComputeCompleted); computeFuture = newComputeFuture; } finally { lock.writeLock().unlock(); } } private void whenComputeCompleted(@Nullable V newValue, @Nullable Throwable error) { lock.writeLock().lock(); try { computeFuture = null; if (error instanceof CancellationException) { return; } var previousValue = value; value = newValue; try { if (listener != null) { listener.afterComputedValueRefreshed(previousValue, newValue); } } finally { if (error != null) { valueFuture.completeExceptionally(error); } else { valueFuture.complete(newValue); } } } finally { lock.writeLock().unlock(); } } private CompletableFuture getValueFuture() { lock.readLock().lock(); try { return valueFuture; } finally { lock.readLock().unlock(); } } public void cancel() { lock.writeLock().lock(); try { if (computeFuture != null) { computeFuture.cancel(false); computeFuture = null; } valueFuture.cancel(false); } finally { lock.writeLock().unlock(); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/SmartCancelableLoadingCache.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import com.google.common.util.concurrent.MoreExecutors; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; /** * A cache with async computation of values, and supporting cancellation. * "Smart" because when a computation is cancelled, it will return to the previous callers the result of the new computation. */ public class SmartCancelableLoadingCache implements AutoCloseable { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ExecutorServiceShutdownWatchable executorService; private final String threadName; private final BiFunction valueComputer; private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); @Nullable private final Listener listener; public interface Listener { void afterCachedValueRefreshed(K key, @Nullable V oldValue, @Nullable V newValue); } public SmartCancelableLoadingCache(String threadName, BiFunction valueComputer) { this(threadName, valueComputer, null); } public SmartCancelableLoadingCache(String threadName, BiFunction valueComputer, @Nullable Listener listener) { this.executorService = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadExecutor(threadName)); this.threadName = threadName; this.valueComputer = valueComputer; this.listener = listener; } /** * Clear the cached value for this key. Attempt to cancel the computation if it is still running. * Awaiting #get() will throw a {@link CancellationException}. */ public void clear(K key) { var valueAndComputeFutures = cache.remove(key); if (valueAndComputeFutures != null) { valueAndComputeFutures.cancel(); } } /** * Force a new computation for this key. Ensure {@link Listener#afterCachedValueRefreshed(Object, Object, Object)} is called. * Awaiting #get() will receive the newly computed value */ public void refreshAsync(K key) { cache.compute(key, (k, v) -> { if (v == null) { return newValueAndScheduleComputation(k); } else { v.scheduleComputationAsync(); return v; } }); } public V get(K key) { return cache.computeIfAbsent(key, this::newValueAndScheduleComputation).get(); } private DebounceComputer newValueAndScheduleComputation(K k) { var value = new DebounceComputer<>(c -> valueComputer.apply(k, c), executorService, (oldValue, newValue) -> { if (listener != null && !Objects.equals(oldValue, newValue)) { listener.afterCachedValueRefreshed(k, oldValue, newValue); } }); value.scheduleComputationAsync(); return value; } @Override public void close() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop " + threadName + " executor service in a timely manner"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/commons/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.commons; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/SonarQubeClient.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.connection; import java.time.Instant; import java.time.Period; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; public class SonarQubeClient { private static final Period WRONG_TOKEN_NOTIFICATION_INTERVAL = Period.ofDays(1); private final String connectionId; private final ServerApi serverApi; private final Either credentials; private final SonarLintRpcClient client; private SonarQubeClientState state = SonarQubeClientState.ACTIVE; @Nullable private Instant lastNotificationTime; public SonarQubeClient(String connectionId, ServerApi serverApi, Either credentials, SonarLintRpcClient client) { this.connectionId = connectionId; this.serverApi = serverApi; this.credentials = credentials; this.client = client; } public Either getCredentials() { return credentials; } public boolean isActive() { return state == SonarQubeClientState.ACTIVE; } public T withClientApiAndReturn(Function serverApiConsumer) { try { var result = serverApiConsumer.apply(serverApi); state = SonarQubeClientState.ACTIVE; lastNotificationTime = null; return result; } catch (UnauthorizedException e) { state = SonarQubeClientState.INVALID_CREDENTIALS; notifyClientAboutWrongTokenIfNeeded(); } return null; } public void withClientApi(Consumer serverApiConsumer) { try { serverApiConsumer.accept(serverApi); state = SonarQubeClientState.ACTIVE; lastNotificationTime = null; } catch (UnauthorizedException e) { state = SonarQubeClientState.INVALID_CREDENTIALS; notifyClientAboutWrongTokenIfNeeded(); } } private boolean shouldNotifyAboutWrongToken() { if (state != SonarQubeClientState.INVALID_CREDENTIALS && state != SonarQubeClientState.MISSING_PERMISSION) { return false; } if (lastNotificationTime == null) { return true; } return lastNotificationTime.plus(WRONG_TOKEN_NOTIFICATION_INTERVAL).isBefore(Instant.now()); } private void notifyClientAboutWrongTokenIfNeeded() { if (shouldNotifyAboutWrongToken()) { client.invalidToken(new InvalidTokenParams(connectionId)); lastNotificationTime = Instant.now(); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/SonarQubeClientState.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.connection; public enum SonarQubeClientState { ACTIVE, INVALID_CREDENTIALS, MISSING_PERMISSION } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/connection/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.connection; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/AnalyzeFileListRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import com.google.gson.Gson; import java.io.IOException; import java.net.URI; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; public class AnalyzeFileListRequestHandler implements HttpRequestHandler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final AnalysisService analysisService; private final ClientFileSystemService clientFileSystemService; private final TaintVulnerabilityTrackingService taintService; private final Gson gson = new Gson(); public AnalyzeFileListRequestHandler(AnalysisService analysisService, ClientFileSystemService clientFileSystemService, TaintVulnerabilityTrackingService taintService) { this.analysisService = analysisService; this.clientFileSystemService = clientFileSystemService; this.taintService = taintService; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext httpContext) throws HttpException, IOException { LOG.debug("Received request for analyzing a list of files"); if (!Method.POST.isSame(request.getMethod())) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } AnalyzeFileListRequest analysisRequest; try { var requestBody = EntityUtils.toString(request.getEntity(), "UTF-8"); analysisRequest = gson.fromJson(requestBody, AnalyzeFileListRequest.class); } catch (Exception e) { LOG.warn("Failed to parse analyze file list request", e); response.setCode(HttpStatus.SC_BAD_REQUEST); response.setEntity(new StringEntity("Failed to parse analyze file list request", ContentType.APPLICATION_JSON)); return; } if (analysisRequest == null || analysisRequest.fileAbsolutePaths == null || analysisRequest.fileAbsolutePaths.isEmpty()) { LOG.warn("Empty or invalid file list in analyze request"); response.setCode(HttpStatus.SC_BAD_REQUEST); response.setEntity(new StringEntity("Empty or invalid file list in analyze request", ContentType.APPLICATION_JSON)); return; } try { response.setEntity(new StringEntity(new Gson().toJson(analyze(analysisRequest)), ContentType.APPLICATION_JSON)); } catch (Exception e) { LOG.error("Failed to analyze files", e); response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setEntity(new StringEntity("Failed to analyze files, reason: " + e.getMessage(), ContentType.APPLICATION_JSON)); } } private AnalyzeFileListResult analyze(AnalyzeFileListRequest request) { var cancelMonitor = new SonarLintCancelMonitor(); var filePaths = request.fileAbsolutePaths.stream() .map(path -> Paths.get(path).toUri().normalize()) .collect(Collectors.toSet()); LOG.debug("Analyzing list of {} files: {}", filePaths.size(), filePaths); var filesByScope = clientFileSystemService.groupFilesByConfigScope(filePaths); if (filesByScope.isEmpty()) { LOG.warn("No files belong to any configured scope, skipping analysis"); throw new IllegalStateException("No files were found to be indexed by SonarQube for IDE"); } var allIssues = filesByScope.entrySet().stream().flatMap(entry -> { var configScopeId = entry.getKey(); var files = entry.getValue(); LOG.info("Analyzing list of {} files: {}", files.size(), files); try { var taints = getTaintsAsRawFindings(configScopeId, files, cancelMonitor); var issues = getIssuesAndHotspotsAsRawFindings(configScopeId, files, cancelMonitor); return Stream.concat(taints, issues); } catch (ExecutionException | InterruptedException e) { LOG.error("Failed to analyze files for config scope {}", configScopeId, e); throw new RuntimeException(e); } }).toList(); return new AnalyzeFileListResult(allIssues); } private Stream getTaintsAsRawFindings(String configScopeId, Set files, SonarLintCancelMonitor cancelMonitor) { return taintService.listAll(configScopeId, true, cancelMonitor) .stream() .filter(taint -> files.contains(taint.getIdeFilePath().toUri())) .map(taint -> { var isMqrMode = taint.getSeverityMode().isRight(); var textRange = taint.getTextRange(); return new RawFindingResponse( taint.getRuleKey(), taint.getMessage(), isMqrMode ? taint.getSeverityMode().getRight().getImpacts().stream() .map(impact -> impact.getImpactSeverity().name()) .collect(Collectors.joining(",")) : taint.getSeverityMode().getLeft().getSeverity().name(), taint.getIdeFilePath().toString(), textRange == null ? null : new TextRange(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset()) ); }); } private Stream getIssuesAndHotspotsAsRawFindings(String configScopeId, Set files, SonarLintCancelMonitor cancelMonitor) throws ExecutionException, InterruptedException { return analysisService.scheduleAnalysis(configScopeId, UUID.randomUUID(), files, Collections.emptyMap(), false, TriggerType.FORCED, cancelMonitor) .thenApplyAsync(results -> results.rawIssues().stream().map(i -> new RawFindingResponse( i.getRuleKey(), i.getMessage(), i.getSeverity() == null ? null : i.getSeverity().name(), i.getFileUri() == null ? null : i.getFileUri().getPath(), i.getTextRange() ))) .get(); } public record AnalyzeFileListRequest(List fileAbsolutePaths) { } public record AnalyzeFileListResult(List findings) { } public record RawFindingResponse(String ruleKey, String message, @Nullable String severity, @Nullable String filePath, @Nullable TextRange textRange) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/AttributeUtils.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import java.util.Collections; import java.util.Map; import java.util.Optional; import org.apache.hc.core5.http.protocol.HttpContext; public class AttributeUtils { public static final String PARAMS_ATTRIBUTE = "params"; public static final String ORIGIN_ATTRIBUTE = "origin"; private AttributeUtils() { } /** * Parsed query parameters of an HTTP request. * Is set in {@link org.sonarsource.sonarlint.core.embedded.server.filter.ParseParamsFilter} */ public static Map getParams(HttpContext context) { return Optional.of(context) .map(c -> (Map) c.getAttribute(PARAMS_ATTRIBUTE)) .orElse(Collections.emptyMap()); } /** * Value of 'Origin' header. * Is set in {@link org.sonarsource.sonarlint.core.embedded.server.filter.RateLimitFilter} and can not be null afterward. */ public static String getOrigin(HttpContext context) { return (String) context.getAttribute(ORIGIN_ATTRIBUTE); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/AwaitingUserTokenFutureRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import static org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository.haveSameOrigin; public class AwaitingUserTokenFutureRepository { private final ConcurrentHashMap> awaitingFuturesByServerUrl = new ConcurrentHashMap<>(); public void addExpectedResponse(String serverBaseUrl, CompletableFuture futureResponse) { var previousFuture = awaitingFuturesByServerUrl.put(serverBaseUrl, futureResponse); if (previousFuture != null) { previousFuture.cancel(false); } futureResponse.whenComplete((r, e) -> awaitingFuturesByServerUrl.remove(serverBaseUrl, futureResponse)); } public Optional> consumeFutureResponse(String serverOrigin) { for (var iterator = awaitingFuturesByServerUrl.entrySet().iterator(); iterator.hasNext();) { var entry = iterator.next(); if (haveSameOrigin(entry.getKey(), serverOrigin)) { iterator.remove(); return Optional.of(entry.getValue()); } } return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/EmbeddedServer.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.net.InetAddress; import java.util.concurrent.TimeUnit; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.io.CloseMode; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.embedded.server.filter.CorsFilter; import org.sonarsource.sonarlint.core.embedded.server.filter.CspFilter; import org.sonarsource.sonarlint.core.embedded.server.filter.ParseParamsFilter; import org.sonarsource.sonarlint.core.embedded.server.filter.RateLimitFilter; import org.sonarsource.sonarlint.core.embedded.server.filter.ValidationFilter; import org.sonarsource.sonarlint.core.embedded.server.handler.GeneratedUserTokenHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowFixSuggestionRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowHotspotRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowIssueRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.StatusRequestHandler; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.embeddedserver.EmbeddedServerStartedParams; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.EMBEDDED_SERVER; public class EmbeddedServer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final int STARTING_PORT = 64120; private static final int ENDING_PORT = 64130; private static final int INVALID_PORT = -1; private HttpServer server; private int port; private final boolean enabled; private final StatusRequestHandler statusRequestHandler; private final GeneratedUserTokenHandler generatedUserTokenHandler; private final ShowHotspotRequestHandler showHotspotRequestHandler; private final ShowIssueRequestHandler showIssueRequestHandler; private final ShowFixSuggestionRequestHandler showFixSuggestionRequestHandler; private final ToggleAutomaticAnalysisRequestHandler toggleAutomaticAnalysisRequestHandler; private final AnalyzeFileListRequestHandler analyzeFileListRequestHandler; private final SonarLintRpcClient client; public EmbeddedServer(InitializeParams params, StatusRequestHandler statusRequestHandler, GeneratedUserTokenHandler generatedUserTokenHandler, ShowHotspotRequestHandler showHotspotRequestHandler, ShowIssueRequestHandler showIssueRequestHandler, ShowFixSuggestionRequestHandler showFixSuggestionRequestHandler, ToggleAutomaticAnalysisRequestHandler toggleAutomaticAnalysisRequestHandler, AnalyzeFileListRequestHandler analyzeFileListRequestHandler, SonarLintRpcClient client) { this.enabled = params.getBackendCapabilities().contains(EMBEDDED_SERVER); this.statusRequestHandler = statusRequestHandler; this.generatedUserTokenHandler = generatedUserTokenHandler; this.showHotspotRequestHandler = showHotspotRequestHandler; this.showIssueRequestHandler = showIssueRequestHandler; this.showFixSuggestionRequestHandler = showFixSuggestionRequestHandler; this.toggleAutomaticAnalysisRequestHandler = toggleAutomaticAnalysisRequestHandler; this.analyzeFileListRequestHandler = analyzeFileListRequestHandler; this.client = client; } @PostConstruct public void start() { if (!enabled) { return; } final var socketConfig = SocketConfig.custom() .setSoTimeout(15, TimeUnit.SECONDS) // let the port be bindable again immediately .setSoReuseAddress(true) .setTcpNoDelay(true) .build(); port = INVALID_PORT; var triedPort = STARTING_PORT; HttpServer startedServer = null; var loopbackAddress = InetAddress.getLoopbackAddress(); while (port < 0 && triedPort <= ENDING_PORT) { try { startedServer = ServerBootstrap.bootstrap() .setLocalAddress(loopbackAddress) .setCanonicalHostName(loopbackAddress.getHostName()) // we will never have long connections .setConnectionReuseStrategy(new DontKeepAliveReuseStrategy()) .setListenerPort(triedPort) .setSocketConfig(socketConfig) .addFilterFirst("RateLimiter", new RateLimitFilter()) .addFilterAfter("RateLimiter", "CORS", new CorsFilter()) .addFilterAfter("CORS", "Params", new ParseParamsFilter()) .addFilterAfter("Params", "Validation", new ValidationFilter(client, SonarCloudActiveEnvironment.prod())) .register("/sonarlint/api/status", statusRequestHandler) .register("/sonarlint/api/token", generatedUserTokenHandler) .register("/sonarlint/api/hotspots/show", showHotspotRequestHandler) .register("/sonarlint/api/issues/show", showIssueRequestHandler) .register("/sonarlint/api/fix/show", showFixSuggestionRequestHandler) .register("/sonarlint/api/analysis/automatic/config", toggleAutomaticAnalysisRequestHandler) .register("/sonarlint/api/analysis/files", analyzeFileListRequestHandler) .addFilterLast("CSP", new CspFilter()) .create(); startedServer.start(); port = triedPort; } catch (Exception t) { LOG.debug("Error while starting port: " + triedPort + ", " + t.getMessage()); triedPort++; if (startedServer != null) { startedServer.close(); } } } if (port > 0) { LOG.info("Started embedded server on port " + port); client.embeddedServerStarted(new EmbeddedServerStartedParams(port)); server = startedServer; } else { LOG.error("Unable to start request handler"); server = null; } } public int getPort() { return port; } public boolean isStarted() { return server != null; } @PreDestroy public void shutdown() { if (isStarted()) { server.close(CloseMode.GRACEFUL); server = null; port = INVALID_PORT; } } private static class DontKeepAliveReuseStrategy implements ConnectionReuseStrategy { @Override public boolean keepAlive(HttpRequest request, HttpResponse response, HttpContext context) { return false; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/RequestHandlerBindingAssistant.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.BindingCandidatesFinder; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; public class RequestHandlerBindingAssistant { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final BindingSuggestionProvider bindingSuggestionProvider; private final BindingCandidatesFinder bindingCandidatesFinder; private final SonarLintRpcClient client; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final ConfigurationRepository configurationRepository; private final ExecutorServiceShutdownWatchable executorService; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; private final ConnectionConfigurationRepository repository; public RequestHandlerBindingAssistant(BindingSuggestionProvider bindingSuggestionProvider, BindingCandidatesFinder bindingCandidatesFinder, SonarLintRpcClient client, ConnectionConfigurationRepository connectionConfigurationRepository, ConfigurationRepository configurationRepository, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, ConnectionConfigurationRepository repository) { this.bindingSuggestionProvider = bindingSuggestionProvider; this.bindingCandidatesFinder = bindingCandidatesFinder; this.client = client; this.connectionConfigurationRepository = connectionConfigurationRepository; this.configurationRepository = configurationRepository; this.executorService = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadExecutor("Show Issue or Hotspot Request Handler")); this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; this.repository = repository; } public interface Callback { void andThen(String connectionId, Collection boundScopes, @Nullable String configurationScopeId, SonarLintCancelMonitor cancelMonitor); } public void assistConnectionAndBindingIfNeededAsync(AssistCreatingConnectionParams connectionParams, String projectKey, String origin, Callback callback) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(executorService); executorService.execute(() -> assistConnectionAndBindingIfNeeded(connectionParams, projectKey, origin, callback, cancelMonitor)); } private void assistConnectionAndBindingIfNeeded(AssistCreatingConnectionParams connectionParams, String projectKey, String origin, Callback callback, SonarLintCancelMonitor cancelMonitor) { var serverUrl = getServerUrl(connectionParams); LOG.debug("Assist connection and binding if needed for project {} and server {}", projectKey, serverUrl); try { var isSonarCloud = connectionParams.getConnectionParams().isRight(); var connectionsMatchingOrigin = isSonarCloud ? connectionConfigurationRepository.findByOrganization(connectionParams.getConnectionParams().getRight().getOrganizationKey()) : connectionConfigurationRepository.findByUrl(serverUrl); if (connectionsMatchingOrigin.isEmpty()) { startFullBindingProcess(); try { var assistNewConnectionResult = assistCreatingConnectionAndWaitForRepositoryUpdate(connectionParams, cancelMonitor); var assistNewBindingResult = assistBindingAndWaitForRepositoryUpdate(assistNewConnectionResult.getNewConnectionId(), isSonarCloud, projectKey, cancelMonitor); var boundScopes = new HashSet(); if (assistNewBindingResult.getConfigurationScopeId() != null) { boundScopes.add(assistNewBindingResult.getConfigurationScopeId()); } callback.andThen(assistNewConnectionResult.getNewConnectionId(), boundScopes, assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); } finally { endFullBindingProcess(); } } else { var isOriginTrusted = repository.hasConnectionWithOrigin(origin); if (isOriginTrusted) { // we pick the first connection but this could lead to issues later if there were several matches (make the user select the right // one?) assistBindingIfNeeded(connectionsMatchingOrigin.get(0).getConnectionId(), isSonarCloud, projectKey, callback, cancelMonitor); } else { LOG.warn("The origin '" + origin + "' is not trusted, this could be a malicious request"); client.showMessage(new ShowMessageParams(MessageType.ERROR, "SonarQube for IDE received a non-trusted request and could not proceed with it. " + "See logs for more details.")); } } } catch (Exception e) { LOG.error("Unable to show issue", e); } } private String getServerUrl(AssistCreatingConnectionParams connectionParams) { return connectionParams.getConnectionParams().isLeft() ? connectionParams.getConnectionParams().getLeft().getServerUrl() : sonarCloudActiveEnvironment.getUri(SonarCloudRegion.valueOf(connectionParams.getConnectionParams().getRight().getRegion().name())).toString(); } private AssistCreatingConnectionResponse assistCreatingConnectionAndWaitForRepositoryUpdate( AssistCreatingConnectionParams connectionParams, SonarLintCancelMonitor cancelMonitor) { var assistNewConnectionResult = assistCreatingConnection(connectionParams, cancelMonitor); // Wait 5s for the connection to be created in the repository. This is happening asynchronously by the // ConnectionService::didUpdateConnections event LOG.debug("Waiting for connection creation notification..."); for (var i = 50; i >= 0; i--) { if (connectionConfigurationRepository.getConnectionsById().containsKey(assistNewConnectionResult.getNewConnectionId())) { break; } sleep(); } if (!connectionConfigurationRepository.getConnectionsById().containsKey(assistNewConnectionResult.getNewConnectionId())) { LOG.warn("Did not receive connection creation notification on a timely manner"); throw new CancellationException(); } return assistNewConnectionResult; } private void assistBindingIfNeeded(String connectionId, boolean isSonarCloud, String projectKey, Callback callback, SonarLintCancelMonitor cancelMonitor) { var scopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); if (scopes.isEmpty()) { var assistNewBindingResult = assistBindingAndWaitForRepositoryUpdate(connectionId, isSonarCloud, projectKey, cancelMonitor); var boundScopes = new HashSet(); if (assistNewBindingResult.getConfigurationScopeId() != null) { boundScopes.add(assistNewBindingResult.getConfigurationScopeId()); } callback.andThen(connectionId, boundScopes, assistNewBindingResult.getConfigurationScopeId(), cancelMonitor); } else { var boundScopes = scopes.stream().map(BoundScope::getConfigScopeId).filter(Objects::nonNull).collect(Collectors.toSet()); // we pick the first bound scope but this could lead to issues later if there were several matches (make the user select the right one?) callback.andThen(connectionId, boundScopes, scopes.iterator().next().getConfigScopeId(), cancelMonitor); } } private NewBinding assistBindingAndWaitForRepositoryUpdate(String connectionId, boolean isSonarCloud, String projectKey, SonarLintCancelMonitor cancelMonitor) { var assistNewBindingResult = assistBinding(connectionId, isSonarCloud, projectKey, cancelMonitor); // Wait 5s for the binding to be created in the repository. This is happening asynchronously by the // ConfigurationService::didUpdateBinding event var configurationScopeId = assistNewBindingResult.getConfigurationScopeId(); if (configurationScopeId != null) { LOG.debug("Waiting for binding creation notification..."); for (var i = 50; i >= 0; i--) { if (configurationRepository.getEffectiveBinding(configurationScopeId).isPresent()) { break; } sleep(); } if (configurationRepository.getEffectiveBinding(configurationScopeId).isEmpty()) { LOG.warn("Did not receive binding creation notification on a timely manner"); throw new CancellationException(); } } return assistNewBindingResult; } private static void sleep() { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new CancellationException("Interrupted!"); } } void startFullBindingProcess() { // we don't want binding suggestions to appear in the middle of a full binding creation process (connection + binding) // the other possibility would be to still notify the client anyway and let it handle UI interactions one at a time (assists, messages, // suggestions, ...) bindingSuggestionProvider.disable(); } void endFullBindingProcess() { bindingSuggestionProvider.enable(); } AssistCreatingConnectionResponse assistCreatingConnection(AssistCreatingConnectionParams connectionParams, SonarLintCancelMonitor cancelMonitor) { var future = client.assistCreatingConnection(connectionParams); cancelMonitor.onCancel(() -> future.cancel(true)); return future.join(); } NewBinding assistBinding(String connectionId, boolean isSonarCloud, String projectKey, SonarLintCancelMonitor cancelMonitor) { var configScopeCandidates = bindingCandidatesFinder.findConfigScopesToBind(connectionId, projectKey, cancelMonitor); // For now, we decided to only support automatic binding if there is only one clear candidate if (configScopeCandidates.size() != 1) { client.noBindingSuggestionFound(new NoBindingSuggestionFoundParams(escapeHtml4(projectKey), isSonarCloud)); return new NewBinding(connectionId, null); } var bindableConfig = configScopeCandidates.iterator().next(); var future = client.assistBinding(new AssistBindingParams(connectionId, projectKey, bindableConfig.getConfigurationScope().id(), bindableConfig.getOrigin())); cancelMonitor.onCancel(() -> future.cancel(true)); var response = future.join(); return new NewBinding(connectionId, response.getConfigurationScopeId()); } static class NewBinding { private final String connectionId; private final String configurationScopeId; private NewBinding(String connectionId, @Nullable String configurationScopeId) { this.connectionId = connectionId; this.configurationScopeId = configurationScopeId; } public String getConnectionId() { return connectionId; } @CheckForNull public String getConfigurationScopeId() { return configurationScopeId; } } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop show issue request handler executor service in a timely manner"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ToggleAutomaticAnalysisRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import com.google.gson.Gson; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.net.URIBuilder; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class ToggleAutomaticAnalysisRequestHandler implements HttpRequestHandler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final AnalysisService analysisService; private final Gson gson = new Gson(); public ToggleAutomaticAnalysisRequestHandler(AnalysisService analysisService) { this.analysisService = analysisService; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { LOG.debug("Received request for toggling automatic analysis"); if (!Method.POST.isSame(request.getMethod())) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } var params = new HashMap(); try { new URIBuilder(request.getUri(), StandardCharsets.UTF_8) .getQueryParams() .forEach(p -> params.put(p.getName(), p.getValue())); } catch (URISyntaxException e) { handleError(response, "Invalid URI"); return; } var enabledParam = params.get("enabled"); if (enabledParam == null) { handleError(response, "Missing 'enabled' query parameter"); return; } boolean enabled; try { enabled = Boolean.parseBoolean(enabledParam); } catch (Exception e) { handleError(response, "Invalid 'enabled' parameter value"); return; } try { analysisService.didChangeAutomaticAnalysisSetting(enabled); response.setCode(HttpStatus.SC_OK); } catch (Exception e) { LOG.error("Failed to toggle automatic analysis", e); response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); var errorResponse = new ErrorMessage("Failed to toggle automatic analysis: " + e.getMessage()); response.setEntity(new StringEntity(gson.toJson(errorResponse), ContentType.APPLICATION_JSON)); } } private void handleError(ClassicHttpResponse response, String clientMessage) { response.setCode(HttpStatus.SC_BAD_REQUEST); var errorResponse = new ErrorMessage(clientMessage); response.setEntity(new StringEntity(gson.toJson(errorResponse), ContentType.APPLICATION_JSON)); } public record ErrorMessage(String message) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/CorsFilter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import java.io.IOException; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpFilterHandler; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; public class CorsFilter implements HttpFilterHandler { @Override public void handle(ClassicHttpRequest request, HttpFilterChain.ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain) throws HttpException, IOException { var origin = AttributeUtils.getOrigin(context); if (Method.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { var response = new BasicClassicHttpResponse(HttpStatus.SC_OK); response.addHeader("Access-Control-Allow-Origin", origin); response.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.addHeader("Access-Control-Allow-Private-Network", true); responseTrigger.submitResponse(response); } else { chain.proceed(request, new HttpFilterChain.ResponseTrigger() { @Override public void sendInformation(ClassicHttpResponse classicHttpResponse) throws HttpException, IOException { responseTrigger.sendInformation(classicHttpResponse); } @Override public void submitResponse(ClassicHttpResponse classicHttpResponse) throws HttpException, IOException { classicHttpResponse.addHeader("Access-Control-Allow-Origin", origin); responseTrigger.submitResponse(classicHttpResponse); } }, context); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/CspFilter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import java.io.IOException; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpFilterHandler; import org.apache.hc.core5.http.protocol.HttpContext; public class CspFilter implements HttpFilterHandler { @Override public void handle(ClassicHttpRequest request, HttpFilterChain.ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain) throws HttpException, IOException { chain.proceed(request, new HttpFilterChain.ResponseTrigger() { @Override public void sendInformation(ClassicHttpResponse classicHttpResponse) throws HttpException, IOException { responseTrigger.sendInformation(classicHttpResponse); } @Override public void submitResponse(ClassicHttpResponse response) throws HttpException, IOException { if (response.getCode() >= HttpStatus.SC_BAD_REQUEST) { responseTrigger.submitResponse(response); return; } var port = request.getAuthority().getPort(); response.setHeader("Content-Security-Policy-Report-Only", "connect-src 'self' http://localhost:" + port + ";"); responseTrigger.submitResponse(response); } }, context); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/ParseParamsFilter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpFilterHandler; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.net.URIBuilder; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; public class ParseParamsFilter implements HttpFilterHandler { @Override public void handle(ClassicHttpRequest request, HttpFilterChain.ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain) throws HttpException, IOException { context.setAttribute(AttributeUtils.PARAMS_ATTRIBUTE, parseParams(request)); chain.proceed(request, responseTrigger, context); } private static Map parseParams(ClassicHttpRequest request) { try { return new URIBuilder(request.getUri(), StandardCharsets.UTF_8) .getQueryParams() .stream() .collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); } catch (URISyntaxException e) { // Ignored } return new HashMap<>(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/RateLimitFilter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpFilterHandler; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; public class RateLimitFilter implements HttpFilterHandler { private static final int MAX_REQUESTS_PER_ORIGIN = 10; private static final long TIME_FRAME_MS = TimeUnit.SECONDS.toMillis(10); private final ConcurrentHashMap requestCounters = new ConcurrentHashMap<>(); @Override public void handle(ClassicHttpRequest request, HttpFilterChain.ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain) throws HttpException, IOException { var originHeader = request.getHeader("Origin"); var origin = originHeader != null ? originHeader.getValue() : null; if (origin == null) { var response = new BasicClassicHttpResponse(HttpStatus.SC_BAD_REQUEST); responseTrigger.submitResponse(response); } else { if (!isRequestAllowed(origin)) { var response = new BasicClassicHttpResponse(HttpStatus.SC_TOO_MANY_REQUESTS); responseTrigger.submitResponse(response); } else { context.setAttribute(AttributeUtils.ORIGIN_ATTRIBUTE, origin); chain.proceed(request, responseTrigger, context); } } } private boolean isRequestAllowed(String origin) { long currentTime = System.currentTimeMillis(); var counter = requestCounters.computeIfAbsent(origin, k -> new RequestCounter(currentTime)); requestCounters.compute(origin, (k, v) -> { if (currentTime - counter.timestamp > TIME_FRAME_MS) { counter.timestamp = currentTime; counter.count = 1; } else { counter.count++; } return counter; }); return counter.count <= MAX_REQUESTS_PER_ORIGIN; } private static class RequestCounter { long timestamp; int count; RequestCounter(long timestamp) { this.timestamp = timestamp; this.count = 0; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/ValidationFilter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import java.io.IOException; import org.apache.commons.lang3.Strings; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.io.HttpFilterHandler; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import static org.apache.hc.core5.http.io.HttpFilterChain.ResponseTrigger; public class ValidationFilter implements HttpFilterHandler { private final SonarLintRpcClient client; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; public ValidationFilter(SonarLintRpcClient client, SonarCloudActiveEnvironment sonarCloudActiveEnvironment) { this.client = client; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; } @Override public void handle(ClassicHttpRequest request, ResponseTrigger responseTrigger, HttpContext context, HttpFilterChain chain) throws HttpException, IOException { var origin = AttributeUtils.getOrigin(context); boolean isSonarCloud = sonarCloudActiveEnvironment.isSonarQubeCloud(origin); var params = AttributeUtils.getParams(context); if (!isSonarCloud && params.containsKey("server")) { var serverUrl = params.get("server"); if (Strings.CI.startsWithAny(serverUrl, SonarCloudRegion.CLOUD_URLS)) { var response = new BasicClassicHttpResponse(HttpStatus.SC_BAD_REQUEST); client.showMessage(new ShowMessageParams(MessageType.ERROR, "Invalid request to SonarQube backend. " + "The 'server' parameter should not be SonarQube Cloud URL, use it only to specify URL of a SonarQube Server.")); responseTrigger.submitResponse(response); } } chain.proceed(request, responseTrigger, context); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/filter/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.embedded.server.filter; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/GeneratedUserTokenHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import com.google.gson.Gson; import java.io.IOException; import java.util.concurrent.CompletableFuture; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.embedded.server.AwaitingUserTokenFutureRepository; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import static java.util.function.Predicate.not; public class GeneratedUserTokenHandler implements HttpRequestHandler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final AwaitingUserTokenFutureRepository awaitingUserTokenFutureRepository; public GeneratedUserTokenHandler(AwaitingUserTokenFutureRepository awaitingUserTokenFutureRepository) { this.awaitingUserTokenFutureRepository = awaitingUserTokenFutureRepository; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { if (!Method.POST.isSame(request.getMethod())) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } String token = extractAndValidateToken(request); if (token == null) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } var origin = AttributeUtils.getOrigin(context); awaitingUserTokenFutureRepository.consumeFutureResponse(origin) .filter(not(CompletableFuture::isCancelled)) .ifPresentOrElse(pendingFuture -> { pendingFuture.complete(new HelpGenerateUserTokenResponse(token)); response.setCode(HttpStatus.SC_OK); response.setEntity(new StringEntity("OK")); }, () -> response.setCode(HttpStatus.SC_FORBIDDEN)); } private static String extractAndValidateToken(ClassicHttpRequest request) throws IOException, ParseException { var requestEntityString = EntityUtils.toString(request.getEntity(), "UTF-8"); String token = null; try { token = new Gson().fromJson(requestEntityString, TokenPayload.class).token; } catch (Exception e) { // will be converted to HTTP response later LOG.error("Could not deserialize user token", e); } return token; } private static class TokenPayload { private String token; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/ShowFixSuggestionRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import com.google.common.annotations.VisibleForTesting; import com.google.gson.Gson; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.apache.commons.lang3.Strings; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.event.FixSuggestionReceivedEvent; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarQubeConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.ChangesDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FileEditDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.LineRangeDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.ShowFixSuggestionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion; import org.springframework.context.ApplicationEventPublisher; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; import static org.sonarsource.sonarlint.core.commons.util.StringUtils.sanitizeAgainstRTLO; public class ShowFixSuggestionRequestHandler implements HttpRequestHandler { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final ApplicationEventPublisher eventPublisher; private final RequestHandlerBindingAssistant requestHandlerBindingAssistant; private final PathTranslationService pathTranslationService; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; private final ClientFileSystemService clientFs; public ShowFixSuggestionRequestHandler(SonarLintRpcClient client, ApplicationEventPublisher eventPublisher, RequestHandlerBindingAssistant requestHandlerBindingAssistant, PathTranslationService pathTranslationService, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, ClientFileSystemService clientFs) { this.client = client; this.eventPublisher = eventPublisher; this.requestHandlerBindingAssistant = requestHandlerBindingAssistant; this.pathTranslationService = pathTranslationService; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; this.clientFs = clientFs; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { var origin = AttributeUtils.getOrigin(context); var showFixSuggestionQuery = extractQuery(request, origin, AttributeUtils.getParams(context)); if (!Method.POST.isSame(request.getMethod()) || !showFixSuggestionQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } eventPublisher.publishEvent(new FixSuggestionReceivedEvent(showFixSuggestionQuery.getFixSuggestion().suggestionId, showFixSuggestionQuery.isSonarCloud ? AiSuggestionSource.SONARCLOUD : AiSuggestionSource.SONARQUBE, showFixSuggestionQuery.fixSuggestion.fileEdit.changes.size(), false)); AssistCreatingConnectionParams serverConnectionParams = createAssistServerConnectionParams(showFixSuggestionQuery, sonarCloudActiveEnvironment); requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync( serverConnectionParams, showFixSuggestionQuery.projectKey, origin, (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { if (doesClientFileExists(configScopeId, showFixSuggestionQuery.fixSuggestion.fileEdit.path, boundScopes)) { showFixSuggestionForScope(configScopeId, showFixSuggestionQuery.issueKey, showFixSuggestionQuery.fixSuggestion); } else { client.showMessage(new ShowMessageParams(MessageType.ERROR, "Attempted to show a fix suggestion for a file that is " + "not known by SonarQube for IDE")); } } }); response.setCode(HttpStatus.SC_OK); response.setEntity(new StringEntity("OK")); } private boolean doesClientFileExists(String configScopeId, String filePath, Collection boundScopes) { var optTranslation = pathTranslationService.getOrComputePathTranslation(configScopeId); if (optTranslation.isPresent()) { var translation = optTranslation.get(); var idePath = translation.serverToIdePath(Paths.get(filePath)); for (var scope : boundScopes) { for (var file : clientFs.getFiles(scope)) { if (Path.of(file.getUri()).endsWith(idePath)) { return true; } } } } return false; } private static AssistCreatingConnectionParams createAssistServerConnectionParams(ShowFixSuggestionQuery query, SonarCloudActiveEnvironment sonarCloudActiveEnvironment) { String tokenName = query.getTokenName(); String tokenValue = query.getTokenValue(); if (query.isSonarCloud) { // If 'isSonarCloud' check passed, we are sure we will have a region var region = sonarCloudActiveEnvironment.getRegionOrThrow(query.getServerUrl()); return new AssistCreatingConnectionParams(new SonarCloudConnectionParams(query.getOrganizationKey(), tokenName, tokenValue, SonarCloudRegion.valueOf(region.name()))); } else { return new AssistCreatingConnectionParams(new SonarQubeConnectionParams(query.getServerUrl(), tokenName, tokenValue)); } } private void showFixSuggestionForScope(String configScopeId, String issueKey, FixSuggestionPayload fixSuggestion) { pathTranslationService.getOrComputePathTranslation(configScopeId).ifPresent(translation -> { var fixSuggestionDto = new FixSuggestionDto( fixSuggestion.suggestionId, fixSuggestion.explanation(), new FileEditDto( translation.serverToIdePath(Paths.get(fixSuggestion.fileEdit.path)), fixSuggestion.fileEdit.changes.stream().map(c -> new ChangesDto( new LineRangeDto(c.beforeLineRange.startLine, c.beforeLineRange.endLine), c.before, c.after) ).toList() ) ); client.showFixSuggestion(new ShowFixSuggestionParams(configScopeId, issueKey, fixSuggestionDto)); }); } @VisibleForTesting ShowFixSuggestionQuery extractQuery(ClassicHttpRequest request, String origin, Map params) throws HttpException, IOException { var payload = extractAndValidatePayload(request); boolean isSonarCloud = sonarCloudActiveEnvironment.isSonarQubeCloud(origin); String serverUrl; if (isSonarCloud) { serverUrl = Strings.CS.removeEnd(origin, "/"); } else { serverUrl = params.get("server"); } return new ShowFixSuggestionQuery(serverUrl, params.get("project"), params.get("issue"), params.get("branch"), params.get("tokenName"), params.get("tokenValue"), params.get("organizationKey"), isSonarCloud, payload); } private static FixSuggestionPayload extractAndValidatePayload(ClassicHttpRequest request) throws IOException, ParseException { var requestEntityString = EntityUtils.toString(request.getEntity(), "UTF-8"); FixSuggestionPayload payload = null; try { payload = new Gson().fromJson(requestEntityString, FixSuggestionPayload.class); } catch (Exception e) { // will be converted to HTTP response later LOG.error("Could not deserialize fix suggestion payload", e); } return payload; } @VisibleForTesting public static class ShowFixSuggestionQuery { private final String serverUrl; private final String projectKey; private final String issueKey; @Nullable private final String branch; @Nullable private final String tokenName; @Nullable private final String tokenValue; @Nullable private final String organizationKey; private final boolean isSonarCloud; private final FixSuggestionPayload fixSuggestion; public ShowFixSuggestionQuery(@Nullable String serverUrl, String projectKey, String issueKey, @Nullable String branch, @Nullable String tokenName, @Nullable String tokenValue, @Nullable String organizationKey, boolean isSonarCloud, FixSuggestionPayload fixSuggestion) { this.serverUrl = serverUrl; this.projectKey = projectKey; this.issueKey = issueKey; this.branch = branch; this.tokenName = tokenName; this.tokenValue = tokenValue; this.organizationKey = organizationKey; this.isSonarCloud = isSonarCloud; this.fixSuggestion = fixSuggestion; } public boolean isValid() { return isNotBlank(projectKey) && isNotBlank(issueKey) && (isSonarCloud || isNotBlank(serverUrl)) && (!isSonarCloud || isNotBlank(organizationKey)) && fixSuggestion.isValid() && isTokenValid(); } /** * Either we get a token combination or we don't get a token combination: There is nothing in between */ public boolean isTokenValid() { if (tokenName != null && tokenValue != null) { return isNotEmpty(tokenName) && isNotEmpty(tokenValue); } return tokenName == null && tokenValue == null; } public String getServerUrl() { return serverUrl; } public String getProjectKey() { return projectKey; } @Nullable public String getOrganizationKey() { return organizationKey; } public String getIssueKey() { return issueKey; } @Nullable public String getBranch() { return branch; } @Nullable public String getTokenName() { return tokenName; } @Nullable public String getTokenValue() { return tokenValue; } public FixSuggestionPayload getFixSuggestion() { return fixSuggestion; } } @VisibleForTesting public record FixSuggestionPayload(FileEditPayload fileEdit, String suggestionId, String explanation) { public FixSuggestionPayload(FileEditPayload fileEdit, String suggestionId, String explanation) { this.fileEdit = fileEdit; this.suggestionId = suggestionId; this.explanation = escapeHtml4(explanation); } public boolean isValid() { return fileEdit.isValid() && !suggestionId.isBlank(); } } @VisibleForTesting public record FileEditPayload(List changes, String path) { public boolean isValid() { return !path.isBlank() && changes.stream().allMatch(ChangesPayload::isValid); } } @VisibleForTesting public record ChangesPayload(TextRangePayload beforeLineRange, String before, String after) { public ChangesPayload(TextRangePayload beforeLineRange, String before, String after) { this.beforeLineRange = beforeLineRange; this.before = sanitizeAgainstRTLO(before); this.after = sanitizeAgainstRTLO(after); } public boolean isValid() { return beforeLineRange.isValid(); } } @VisibleForTesting public record TextRangePayload(int startLine, int endLine) { public boolean isValid() { return startLine >= 0 && endLine >= 0 && startLine <= endLine; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/ShowHotspotRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import java.io.IOException; import java.util.Map; import java.util.Optional; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarQubeConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.ShowHotspotParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspotDetails; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class ShowHotspotRequestHandler implements HttpRequestHandler { private final SonarLintRpcClient client; private final SonarQubeClientManager sonarQubeClientManager; private final TelemetryService telemetryService; private final RequestHandlerBindingAssistant requestHandlerBindingAssistant; private final PathTranslationService pathTranslationService; public ShowHotspotRequestHandler(SonarLintRpcClient client, SonarQubeClientManager sonarQubeClientManager, TelemetryService telemetryService, RequestHandlerBindingAssistant requestHandlerBindingAssistant, PathTranslationService pathTranslationService) { this.client = client; this.sonarQubeClientManager = sonarQubeClientManager; this.telemetryService = telemetryService; this.requestHandlerBindingAssistant = requestHandlerBindingAssistant; this.pathTranslationService = pathTranslationService; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { var origin = AttributeUtils.getOrigin(context); var showHotspotQuery = extractQuery(AttributeUtils.getParams(context)); if (!Method.GET.isSame(request.getMethod()) || !showHotspotQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } telemetryService.showHotspotRequestReceived(); var sonarQubeConnectionParams = new SonarQubeConnectionParams(showHotspotQuery.serverUrl, null, null); var connectionParams = new AssistCreatingConnectionParams(sonarQubeConnectionParams); requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync(connectionParams, showHotspotQuery.projectKey, origin, (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { showHotspotForScope(connectionId, configScopeId, showHotspotQuery.hotspotKey, cancelMonitor); } }); response.setCode(HttpStatus.SC_OK); response.setEntity(new StringEntity("OK")); } private void showHotspotForScope(String connectionId, String configurationScopeId, String hotspotKey, SonarLintCancelMonitor cancelMonitor) { var hotspotOpt = tryFetchHotspot(connectionId, hotspotKey, cancelMonitor); if (hotspotOpt.isPresent()) { pathTranslationService.getOrComputePathTranslation(configurationScopeId) .ifPresent(translation -> client.showHotspot(new ShowHotspotParams(configurationScopeId, adapt(hotspotKey, hotspotOpt.get(), translation)))); } else { client.showMessage(new ShowMessageParams(MessageType.ERROR, "Could not show the hotspot. See logs for more details")); } } private Optional tryFetchHotspot(String connectionId, String hotspotKey, SonarLintCancelMonitor cancelMonitor) { return sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(connectionId, api -> api.hotspot().fetch(hotspotKey, cancelMonitor)); } private static HotspotDetailsDto adapt(String hotspotKey, ServerHotspotDetails hotspot, FilePathTranslation translation) { return new HotspotDetailsDto( hotspotKey, hotspot.message, translation.serverToIdePath(hotspot.filePath), adapt(hotspot.textRange), hotspot.author, hotspot.status.toString(), hotspot.resolution != null ? hotspot.resolution.toString() : null, adapt(hotspot.rule), hotspot.codeSnippet); } private static HotspotDetailsDto.HotspotRule adapt(ServerHotspotDetails.Rule rule) { return new HotspotDetailsDto.HotspotRule( rule.key, rule.name, rule.securityCategory, rule.vulnerabilityProbability.toString(), rule.riskDescription, rule.vulnerabilityDescription, rule.fixRecommendations); } private static TextRangeDto adapt(TextRange textRange) { return new TextRangeDto(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset()); } private static ShowHotspotQuery extractQuery(Map params) { return new ShowHotspotQuery(params.get("server"), params.get("project"), params.get("hotspot")); } private record ShowHotspotQuery(String serverUrl, String projectKey, String hotspotKey) { public boolean isValid() { return isNotBlank(serverUrl) && isNotBlank(projectKey) && isNotBlank(hotspotKey); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/ShowIssueRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.apache.commons.lang3.Strings; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarCloudConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SonarQubeConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.ShowIssueParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.FlowDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.LocationDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto; import org.sonarsource.sonarlint.core.serverapi.issue.IssueApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.rules.RulesApi; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesSynchronizationService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotEmpty; public class ShowIssueRequestHandler implements HttpRequestHandler { private final SonarLintRpcClient client; private final SonarQubeClientManager sonarQubeClientManager; private final TelemetryService telemetryService; private final RequestHandlerBindingAssistant requestHandlerBindingAssistant; private final PathTranslationService pathTranslationService; private final SonarCloudActiveEnvironment sonarCloudActiveEnvironment; private final SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService; public ShowIssueRequestHandler(SonarLintRpcClient client, SonarQubeClientManager sonarQubeClientManager, TelemetryService telemetryService, RequestHandlerBindingAssistant requestHandlerBindingAssistant, PathTranslationService pathTranslationService, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService) { this.client = client; this.sonarQubeClientManager = sonarQubeClientManager; this.telemetryService = telemetryService; this.requestHandlerBindingAssistant = requestHandlerBindingAssistant; this.pathTranslationService = pathTranslationService; this.sonarCloudActiveEnvironment = sonarCloudActiveEnvironment; this.sonarProjectBranchesSynchronizationService = sonarProjectBranchesSynchronizationService; } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { var origin = AttributeUtils.getOrigin(context); var showIssueQuery = extractQuery(origin, AttributeUtils.getParams(context)); if (!Method.GET.isSame(request.getMethod()) || !showIssueQuery.isValid()) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } telemetryService.showIssueRequestReceived(); AssistCreatingConnectionParams serverConnectionParams = createAssistServerConnectionParams(showIssueQuery); requestHandlerBindingAssistant.assistConnectionAndBindingIfNeededAsync( serverConnectionParams, showIssueQuery.projectKey, origin, (connectionId, boundScopes, configScopeId, cancelMonitor) -> { if (configScopeId != null) { var branch = showIssueQuery.branch; if (branch == null) { branch = sonarProjectBranchesSynchronizationService.findMainBranch(connectionId, showIssueQuery.projectKey, cancelMonitor); } showIssueForScope(connectionId, configScopeId, showIssueQuery.issueKey, showIssueQuery.projectKey, branch, showIssueQuery.pullRequest, cancelMonitor); } }); response.setCode(HttpStatus.SC_OK); response.setEntity(new StringEntity("OK")); } private static AssistCreatingConnectionParams createAssistServerConnectionParams(ShowIssueQuery query) { var tokenName = query.getTokenName(); var tokenValue = query.getTokenValue(); return query.isSonarCloud ? new AssistCreatingConnectionParams(new SonarCloudConnectionParams(query.getOrganizationKey(), tokenName, tokenValue, org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.valueOf(query.getRegion().name()))) : new AssistCreatingConnectionParams(new SonarQubeConnectionParams(query.getServerUrl(), tokenName, tokenValue)); } private void showIssueForScope(String connectionId, String configScopeId, String issueKey, String projectKey, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { var issueDetailsOpt = tryFetchIssue(connectionId, issueKey, projectKey, branch, pullRequest, cancelMonitor); if (issueDetailsOpt.isPresent()) { pathTranslationService.getOrComputePathTranslation(configScopeId) .ifPresent(translation -> client.showIssue(getShowIssueParams(issueDetailsOpt.get(), connectionId, configScopeId, branch, pullRequest, translation, cancelMonitor))); } else { client.showMessage(new ShowMessageParams(MessageType.ERROR, "Could not show the issue. See logs for more details")); } } @VisibleForTesting ShowIssueParams getShowIssueParams(IssueApi.ServerIssueDetails issueDetails, String connectionId, String configScopeId, String branch, @Nullable String pullRequest, FilePathTranslation translation, SonarLintCancelMonitor cancelMonitor) { var flowLocations = issueDetails.flowList.stream().map(flow -> { var locations = flow.getLocationsList().stream().map(location -> { var locationComponent = issueDetails.componentsList.stream().filter(component -> component.getKey().equals(location.getComponent())).findFirst(); var filePath = locationComponent.map(Issues.Component::getPath).orElse(""); var locationTextRange = location.getTextRange(); var codeSnippet = tryFetchCodeSnippet(connectionId, locationComponent.map(Issues.Component::getKey).orElse(""), locationTextRange, branch, pullRequest, cancelMonitor); var locationTextRangeDto = new TextRangeDto(locationTextRange.getStartLine(), locationTextRange.getStartOffset(), locationTextRange.getEndLine(), locationTextRange.getEndOffset()); return new LocationDto(locationTextRangeDto, location.getMsg(), translation.serverToIdePath(Paths.get(filePath)), codeSnippet.orElse("")); }).toList(); return new FlowDto(locations); }).toList(); var textRange = issueDetails.textRange; var textRangeDto = new TextRangeDto(textRange.getStartLine(), textRange.getStartOffset(), textRange.getEndLine(), textRange.getEndOffset()); var isTaint = isIssueTaint(issueDetails.ruleKey); return new ShowIssueParams(configScopeId, new IssueDetailsDto(textRangeDto, issueDetails.ruleKey, issueDetails.key, translation.serverToIdePath(issueDetails.path), issueDetails.message, issueDetails.creationDate, issueDetails.codeSnippet, isTaint, flowLocations)); } static boolean isIssueTaint(String ruleKey) { return RulesApi.TAINT_REPOS.stream().anyMatch(ruleKey::startsWith); } private Optional tryFetchIssue(String connectionId, String issueKey, String projectKey, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { return sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(connectionId, serverApi -> serverApi.issue().fetchServerIssue(issueKey, projectKey, branch, pullRequest, cancelMonitor)); } private Optional tryFetchCodeSnippet(String connectionId, String fileKey, Common.TextRange textRange, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { return sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(connectionId, api -> api.issue().getCodeSnippet(fileKey, textRange, branch, pullRequest, cancelMonitor)); } @VisibleForTesting ShowIssueQuery extractQuery(String origin, Map params) { boolean isSonarCloud = sonarCloudActiveEnvironment.isSonarQubeCloud(origin); String serverUrl; SonarCloudRegion region = null; if (isSonarCloud) { serverUrl = Strings.CS.removeEnd(origin, "/"); region = sonarCloudActiveEnvironment.getRegionOrThrow(serverUrl); } else { serverUrl = params.get("server"); } return new ShowIssueQuery(serverUrl, params.get("project"), params.get("issue"), params.get("branch"), params.get("pullRequest"), params.get("tokenName"), params.get("tokenValue"), params.get("organizationKey"), region, isSonarCloud); } @VisibleForTesting public static class ShowIssueQuery { private final String serverUrl; private final String projectKey; private final String issueKey; @Nullable private final String branch; @Nullable private final String pullRequest; @Nullable private final String tokenName; @Nullable private final String tokenValue; @Nullable private final String organizationKey; @Nullable private final SonarCloudRegion region; private final boolean isSonarCloud; public ShowIssueQuery(@Nullable String serverUrl, String projectKey, String issueKey, @Nullable String branch, @Nullable String pullRequest, @Nullable String tokenName, @Nullable String tokenValue, @Nullable String organizationKey, @Nullable SonarCloudRegion region, boolean isSonarCloud) { this.serverUrl = serverUrl; this.projectKey = projectKey; this.issueKey = issueKey; this.branch = branch; this.pullRequest = pullRequest; this.tokenName = tokenName; this.tokenValue = tokenValue; this.organizationKey = organizationKey; this.region = region != null ? region : SonarCloudRegion.EU; this.isSonarCloud = isSonarCloud; } public boolean isValid() { return isNotBlank(projectKey) && isNotBlank(issueKey) && (isSonarCloud || isNotBlank(serverUrl)) && (!isSonarCloud || isNotBlank(organizationKey)) && isPullRequestParamValid() && isTokenValid(); } public boolean isPullRequestParamValid() { if (pullRequest != null) { return isNotEmpty(pullRequest); } return true; } /** * Either we get a token combination or we don't get a token combination: There is nothing in between */ public boolean isTokenValid() { if (tokenName != null && tokenValue != null) { return isNotEmpty(tokenName) && isNotEmpty(tokenValue); } return tokenName == null && tokenValue == null; } public String getServerUrl() { return serverUrl; } public String getProjectKey() { return projectKey; } @Nullable public String getOrganizationKey() { return organizationKey; } public String getIssueKey() { return issueKey; } @Nullable public String getBranch() { return branch; } @Nullable public String getPullRequest() { return pullRequest; } @Nullable public String getTokenName() { return tokenName; } @Nullable public String getTokenValue() { return tokenValue; } @Nullable public SonarCloudRegion getRegion() { return region; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/StatusRequestHandler.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import com.google.gson.Gson; import com.google.gson.annotations.Expose; import java.io.IOException; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; public class StatusRequestHandler implements HttpRequestHandler { private final SonarLintRpcClient client; private final ConnectionConfigurationRepository repository; private final ClientConstantInfoDto clientInfo; public StatusRequestHandler(SonarLintRpcClient client, ConnectionConfigurationRepository repository, InitializeParams params) { this.client = client; this.repository = repository; this.clientInfo = params.getClientConstantInfo(); } @Override public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException { if (!Method.GET.isSame(request.getMethod())) { response.setCode(HttpStatus.SC_BAD_REQUEST); return; } boolean trustedServer = isTrustedServer(AttributeUtils.getOrigin(context)); var description = getDescription(trustedServer); var capabilities = new CapabilitiesResponse(true); // We need a token when the requesting server is not a trusted one (in order to automatically create a connection). response.setEntity(new StringEntity(new Gson().toJson(new StatusResponse(clientInfo.getName(), description, !trustedServer, capabilities)), ContentType.APPLICATION_JSON)); } private String getDescription(boolean trustedServer) { if (trustedServer) { var getClientInfoResponse = client.getClientLiveInfo().join(); return getClientInfoResponse.getDescription(); } return ""; } private boolean isTrustedServer(String serverOrigin) { return repository.hasConnectionWithOrigin(serverOrigin); } private record StatusResponse(@Expose String ideName, @Expose String description, @Expose boolean needsToken, @Expose CapabilitiesResponse capabilities) { } private record CapabilitiesResponse(@Expose boolean canOpenFixSuggestion) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/handler/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.embedded.server.handler; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.embedded.server; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/BindingConfigChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; public record BindingConfigChangedEvent(String configScopeId, BindingConfiguration previousConfig, BindingConfiguration newConfig) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConfigurationScopeRemovedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; public record ConfigurationScopeRemovedEvent(ConfigurationScope removedConfigurationScope, BindingConfiguration removedBindingConfiguration) { public String getRemovedConfigurationScopeId() { return removedConfigurationScope.id(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConfigurationScopesAddedWithBindingEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScopeWithBinding; public record ConfigurationScopesAddedWithBindingEvent(Set addedConfigurationScopes) { public Set getConfigScopeIds() { return addedConfigurationScopes.stream() .map(configScope -> configScope.scope().id()) .collect(Collectors.toSet()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConnectionConfigurationAddedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.commons.ConnectionKind; public record ConnectionConfigurationAddedEvent(String addedConnectionId, ConnectionKind connectionKind) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConnectionConfigurationRemovedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; public record ConnectionConfigurationRemovedEvent(String removedConnectionId) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConnectionConfigurationUpdatedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; public record ConnectionConfigurationUpdatedEvent(String updatedConnectionId) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ConnectionCredentialsChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; public class ConnectionCredentialsChangedEvent { private final String connectionId; public ConnectionCredentialsChangedEvent(String connectionId) { this.connectionId = connectionId; } public String getConnectionId() { return connectionId; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/DependencyRisksSynchronizedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.storage.UpdateSummary; public record DependencyRisksSynchronizedEvent(String connectionId, String sonarProjectKey, String sonarBranch, UpdateSummary summary) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/FixSuggestionReceivedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; public record FixSuggestionReceivedEvent(String fixSuggestionId, AiSuggestionSource source, int snippetsCount, boolean wasGeneratedFromIde) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/LocalOnlyIssueStatusChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; public class LocalOnlyIssueStatusChangedEvent { private final LocalOnlyIssue issue; public LocalOnlyIssueStatusChangedEvent(LocalOnlyIssue issue) { this.issue = issue; } public LocalOnlyIssue getIssue() { return issue; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/MatchingSessionEndedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; public record MatchingSessionEndedEvent(long newIssuesFound, long issuesFixed) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/PluginStatusUpdateEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import java.util.Collection; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.plugin.PluginStatus; public record PluginStatusUpdateEvent( @Nullable String connectionId, Collection newStatuses) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/ServerIssueStatusChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerFinding; public class ServerIssueStatusChangedEvent { private final String connectionId; private final String projectKey; private final ServerFinding finding; public ServerIssueStatusChangedEvent(String connectionId, String projectKey, ServerFinding finding) { this.connectionId = connectionId; this.projectKey = projectKey; this.finding = finding; } public String getConnectionId() { return connectionId; } public String getProjectKey() { return projectKey; } public ServerFinding getFinding() { return finding; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/SonarServerEventReceivedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; public class SonarServerEventReceivedEvent { private final String connectionId; private final SonarServerEvent event; public SonarServerEventReceivedEvent(String connectionId, SonarServerEvent event) { this.connectionId = connectionId; this.event = event; } public String getConnectionId() { return connectionId; } public SonarServerEvent getEvent() { return event; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/TaintVulnerabilitiesSynchronizedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.serverconnection.storage.UpdateSummary; public class TaintVulnerabilitiesSynchronizedEvent { private final String connectionId; private final String sonarProjectKey; private final String sonarBranch; private final UpdateSummary summary; public TaintVulnerabilitiesSynchronizedEvent(String connectionId, String sonarProjectKey, String sonarBranch, UpdateSummary summary) { this.connectionId = connectionId; this.sonarProjectKey = sonarProjectKey; this.sonarBranch = sonarBranch; this.summary = summary; } public String getConnectionId() { return connectionId; } public String getSonarProjectKey() { return sonarProjectKey; } public String getSonarBranch() { return sonarBranch; } public UpdateSummary getSummary() { return summary; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/TelemetryUpdatedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.event; public record TelemetryUpdatedEvent(boolean isTelemetryEnabled) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/event/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.event; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/file/FilePathTranslation.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import java.nio.file.Path; public class FilePathTranslation { private final Path idePathPrefix; private final Path serverPathPrefix; public FilePathTranslation(Path idePathPrefix, Path serverPathPrefix) { this.idePathPrefix = idePathPrefix; this.serverPathPrefix = serverPathPrefix; } public Path getIdePathPrefix() { return idePathPrefix; } public Path getServerPathPrefix() { return serverPathPrefix; } public Path serverToIdePath(Path serverFilePath) { if (!serverFilePath.toString().startsWith(serverPathPrefix.toString())) { return serverFilePath; } var localPrefixLen = serverPathPrefix.toString().length(); if (localPrefixLen > 0) { localPrefixLen++; } return idePathPrefix.resolve(serverFilePath.toString().substring(localPrefixLen)); } public Path ideToServerPath(Path idePath) { if (!idePath.toString().startsWith(idePathPrefix.toString())) { return idePath; } var localPrefixLen = idePathPrefix.toString().length(); if (localPrefixLen > 0) { localPrefixLen++; } return serverPathPrefix.resolve(idePath.toString().substring(localPrefixLen)); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/file/PathTranslationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import jakarta.annotation.PreDestroy; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.SonarLintMDC; import org.sonarsource.sonarlint.core.branch.MatchedSonarProjectBranchChangedEvent; import org.sonarsource.sonarlint.core.commons.SmartCancelableLoadingCache; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.serverconnection.prefix.FileTreeMatcher; import org.springframework.context.event.EventListener; /** * The path translation service is responsible for matching the files on the server with the files on the client. * This is only used in connected mode. * A debounce mechanism is used to avoid too many requests to the server. */ public class PathTranslationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ClientFileSystemService clientFs; private final ConfigurationRepository configurationRepository; private final ServerFilePathsProvider serverFilePathsProvider; private final SmartCancelableLoadingCache cachedPathsTranslationByConfigScope = new SmartCancelableLoadingCache<>("sonarlint-path-translation", this::computePaths, (key, oldValue, newValue) -> { }); public PathTranslationService(ClientFileSystemService clientFs, ConfigurationRepository configurationRepository, ServerFilePathsProvider serverFilePathsProvider) { this.clientFs = clientFs; this.configurationRepository = configurationRepository; this.serverFilePathsProvider = serverFilePathsProvider; } @CheckForNull private FilePathTranslation computePaths(String configScopeId, SonarLintCancelMonitor cancelMonitor) { SonarLintMDC.putConfigScopeId(configScopeId); LOG.debug("Computing paths translation for config scope '{}'...", configScopeId); var fileMatcher = new FileTreeMatcher(); var binding = configurationRepository.getEffectiveBinding(configScopeId).orElse(null); if (binding == null) { LOG.debug("Config scope '{}' does not exist or is not bound", configScopeId); return null; } return serverFilePathsProvider.getServerPaths(binding, cancelMonitor) .map(paths -> matchPaths(configScopeId, fileMatcher, paths)) .orElse(null); } private FilePathTranslation matchPaths(String configScopeId, FileTreeMatcher fileMatcher, List serverFilePaths) { LOG.debug("Starting matching paths for config scope '{}'...", configScopeId); var localFilePaths = clientFs.getFiles(configScopeId); if (localFilePaths.isEmpty()) { LOG.debug("No client files for config scope '{}'. Skipping path matching.", configScopeId); // Maybe a config scope without files, or the filesystem has not been initialized yet return new FilePathTranslation(Paths.get(""), Paths.get("")); } var match = fileMatcher.match(serverFilePaths, localFilePaths.stream().map(ClientFile::getClientRelativePath).toList()); LOG.debug("Matched paths for config scope '{}':\n * idePrefix={}\n * serverPrefix={}", configScopeId, match.idePrefix(), match.sqPrefix()); return new FilePathTranslation(match.idePrefix(), match.sqPrefix()); } @EventListener public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) { cachedPathsTranslationByConfigScope.clear(event.getRemovedConfigurationScopeId()); } @EventListener public void onBindingChanged(BindingConfigChangedEvent event) { var configScopeId = event.configScopeId(); cachedPathsTranslationByConfigScope.refreshAsync(configScopeId); } @EventListener public void onBranchChanged(MatchedSonarProjectBranchChangedEvent event) { var configScopeId = event.getConfigurationScopeId(); cachedPathsTranslationByConfigScope.refreshAsync(configScopeId); } public Optional getOrComputePathTranslation(String configurationScopeId) { return Optional.ofNullable(cachedPathsTranslationByConfigScope.get(configurationScopeId)); } @PreDestroy public void shutdown() { cachedPathsTranslationByConfigScope.close(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CancellationException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; public class ServerFilePathsProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarQubeClientManager sonarQubeClientManager; private final Map cachedResponseFilePathByBinding = new HashMap<>(); private final Path cacheDirectoryPath; private final Cache> temporaryInMemoryFilePathCacheByBinding; public ServerFilePathsProvider(SonarQubeClientManager sonarQubeClientManager, UserPaths userPaths) { this.sonarQubeClientManager = sonarQubeClientManager; this.cacheDirectoryPath = userPaths.getStorageRoot().resolve("cache"); this.temporaryInMemoryFilePathCacheByBinding = CacheBuilder.newBuilder() .expireAfterWrite(Duration.of(1, ChronoUnit.MINUTES)) .maximumSize(3) .build(); clearCachePath(); } private void clearCachePath() { if (!cacheDirectoryPath.toFile().exists()) { return; } try { FileUtils.deleteDirectory(cacheDirectoryPath.toFile()); } catch (IOException e) { LOG.debug("Error occurred while deleting a cache file", e); } } Optional> getServerPaths(Binding binding, SonarLintCancelMonitor cancelMonitor) { return getPathsFromInMemoryCache(binding) .or(() -> getPathsFromFileCache(binding)) .or(() -> fetchPathsFromServer(binding, cancelMonitor)); } private Optional> getPathsFromInMemoryCache(Binding binding) { return Optional.ofNullable(temporaryInMemoryFilePathCacheByBinding.getIfPresent(binding)); } private Optional> getPathsFromFileCache(Binding binding) { return Optional.ofNullable(cachedResponseFilePathByBinding.get(binding)) .filter(path -> path.toFile().exists()) .map(path -> { List paths = readServerPathsFromFile(path); putToInMemoryCache(binding, paths); return paths; }); } private Optional> fetchPathsFromServer(Binding binding, SonarLintCancelMonitor cancelMonitor) { try { return sonarQubeClientManager.withActiveClientAndReturn(binding.connectionId(), serverApi -> { List paths = fetchPathsFromServer(serverApi, binding.sonarProjectKey(), cancelMonitor); cacheServerPaths(binding, paths); return paths; }); } catch (CancellationException e) { throw e; } catch (Exception e) { LOG.debug("Error while getting server file paths for project '{}'", binding.sonarProjectKey(), e); return Optional.empty(); } } private static List readServerPathsFromFile(Path responsePath) { try { return Files.readAllLines(responsePath).stream().map(Paths::get).toList(); } catch (IOException e) { LOG.debug("Error occurred while reading the file server path response file cache {}", responsePath); return Collections.emptyList(); } } private void putToInMemoryCache(Binding binding, List paths) { temporaryInMemoryFilePathCacheByBinding.put(binding, paths); } private static List fetchPathsFromServer(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { return serverApi.component().getAllFileKeys(projectKey, cancelMonitor).stream() .map(fileKey -> StringUtils.substringAfterLast(fileKey, ":")) .map(Paths::get) .toList(); } private void cacheServerPaths(Binding binding, List paths) { var fileName = UUID.randomUUID().toString(); var filePath = cacheDirectoryPath.resolve(fileName); try { Files.createDirectories(cacheDirectoryPath); writeToFile(filePath, paths); cachedResponseFilePathByBinding.put(binding, filePath); putToInMemoryCache(binding, paths); } catch (IOException e) { LOG.debug("Error occurred while writing the cache file", e); } } private static void writeToFile(Path filePath, List paths) throws IOException { try (var bufferedWriter = new BufferedWriter(new FileWriter(filePath.toFile(), Charset.defaultCharset()))) { for (Path path : paths) { bufferedWriter.write(path + System.lineSeparator()); } } } @VisibleForTesting void clearInMemoryCache() { temporaryInMemoryFilePathCacheByBinding.invalidateAll(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/file/WindowsShortcutUtils.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URI; import java.util.Arrays; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static org.apache.commons.lang3.ArrayUtils.reverse; public class WindowsShortcutUtils { // Based on Windows specification the magic number is 0x0000004C that must be tested with both big and little endian // as it might differ based on the architecture / OS. private static final byte[] WINDOWS_SHORTCUT_MAGIC_NUMBER = new byte[] {0, 0, 0, 76}; private WindowsShortcutUtils() { // utility class } /** * Checks whether a file provided by URI is a Windows shortcut or not. These differ from actual (sym)links on * Windows as they are like an object containing the pointer to the other resource instead of pointing to the * resource directly. */ public static boolean isWindowsShortcut(URI uri) { // Based on the Windows specification the shortcuts have this file suffix, when changing the file suffix they won't // work anymore. So if users would create a shortcut, then rename it and have it in the scope of SonarLint this // would fail but that is fine. if (!uri.toString().contains(".lnk")) { return false; } // If the filename ends with ".lnk" we check the magic number in order to determine it to actually be a windows // shortcut. This is expensive and therefore will actually only do it on files that match this filter! var magicNumber = new byte[4]; try (var is = new FileInputStream(new File(uri))) { if (is.read(magicNumber) != magicNumber.length) { // We can only read 0-3 bytes, therefore it cannot be a Windows shortcut. No idea what kind of file this might // be (e.g. a text file with 3 characters?) but hey, they gave it a ".lnk" suffix and mimicked a shortcut so // they probably know better. return false; } // Check big endian if (Arrays.equals(WINDOWS_SHORTCUT_MAGIC_NUMBER, magicNumber)) { return true; } // Check little endian reverse(magicNumber); return Arrays.equals(WINDOWS_SHORTCUT_MAGIC_NUMBER, magicNumber); } catch (IOException err) { SonarLintLogger.get().debug("Cannot check whether '" + uri + "' is a Windows shortcut, assuming it is not."); } return false; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/file/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.file; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/ClientFile.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import javax.annotation.Nullable; import org.apache.commons.compress.utils.FileNameUtils; import org.apache.commons.io.ByteOrderMark; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.util.FileUtils; public class ClientFile { private static final String SONARLINT_FOLDER_NAME = ".sonarlint"; /** * Unique identifier for this file on the client side */ private final URI uri; private final String configScopeId; /** * Relative path for this file on the client side. We use the {@link Path} class for convenience for filename separators, * but it is not necessary to have a file on the filesystem. */ private final Path relativePath; /** * For some clients, deciding if a file is a test is costly, and will be computed only when a file is opened in an editor. * null means unknown */ @Nullable private final Boolean isTest; @Nullable private final Charset charset; /** * The absolute path on the local filesystem, if available. */ @Nullable private final Path fsPath; @Nullable private final SonarLanguage detectedLanguage; /** * Tell if the file content is flushed on disk? * If the file is dirty, it means that the content is not flushed on disk, and the backend get the content from the client. */ private boolean isDirty; /** * When the file is dirty, the content should be provided by the client. */ @Nullable private String clientProvidedContent; private final boolean isUserDefined; public ClientFile(URI uri, String configScopeId, Path relativePath, @Nullable Boolean isTest, @Nullable Charset charset, @Nullable Path fsPath, @Nullable SonarLanguage detectedLanguage, boolean isUserDefined) { this.uri = uri; this.configScopeId = configScopeId; this.relativePath = relativePath; this.isTest = isTest; this.charset = charset; this.fsPath = fsPath; this.detectedLanguage = detectedLanguage; this.isUserDefined = isUserDefined; } public Path getClientRelativePath() { return relativePath; } public String getFileName() { return relativePath.getFileName().toString(); } public URI getUri() { return uri; } public boolean isDirty() { return isDirty; } public String getContent() { if (isDirty) { return clientProvidedContent; } var charsetToUse = getCharset(); try (var inputStream = inputStream()) { return IOUtils.toString(inputStream, charsetToUse); } catch (IOException e) { throw new IllegalStateException("Unable to read file " + fsPath + "content with charset " + charsetToUse, e); } } public InputStream inputStream() throws IOException { if (isDirty && clientProvidedContent != null) { return new ByteArrayInputStream(clientProvidedContent.getBytes(getCharset())); } if (fsPath == null) { throw new IllegalStateException("File " + uri + " is not dirty or does not have content but has no OS Path defined"); } return BOMInputStream.builder().setInputStream(Files.newInputStream(fsPath)) // order list of BOMs by length descending, as anyway a sort is made in the constructor .setByteOrderMarks(ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE).get(); } public Charset getCharset() { return charset != null ? charset : Charset.defaultCharset(); } public String getConfigScopeId() { return configScopeId; } public void setDirty(String content) { this.isDirty = true; this.clientProvidedContent = content; } public void setClean() { this.isDirty = false; this.clientProvidedContent = null; } public boolean isLargerThan(long size) throws IOException { if (isDirty && clientProvidedContent != null) { return clientProvidedContent.getBytes(getCharset()).length > size; } else { var localPath = FileUtils.getFilePathFromUri(uri); if (Files.exists(localPath)) { return Files.size(localPath) > size; } } return false; } public boolean isSonarlintConfigurationFile() { // Considering .sonarlint/*.json for compatibility with settings exported from Visual Studio return isInDotSonarLintFolder() && hasJsonExtension(); } private boolean isInDotSonarLintFolder() { var sonarlintPath = getClientRelativePath().getParent(); return sonarlintPath != null && SONARLINT_FOLDER_NAME.equals(sonarlintPath.getFileName().toString()); } private boolean hasJsonExtension() { return "json".equals(FileNameUtils.getExtension(getClientRelativePath())); } public boolean isTest() { return Boolean.TRUE == isTest; } @Nullable public SonarLanguage getDetectedLanguage() { return detectedLanguage; } @Override public String toString() { return uri.toString(); } public boolean isUserDefined() { return isUserDefined; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/ClientFileSystemService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import org.sonarsource.sonarlint.core.commons.SmartCancelableLoadingCache; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidUpdateFileSystemParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.GetBaseDirParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.ListFilesParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; public class ClientFileSystemService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient rpcClient; private final ApplicationEventPublisher eventPublisher; private final Map filesByUri = new ConcurrentHashMap<>(); private final Map baseDirPerConfigScopeId = new ConcurrentHashMap<>(); private final OpenFilesRepository openFilesRepository; private final TelemetryService telemetryService; private final SmartCancelableLoadingCache> filesByConfigScopeIdCache = new SmartCancelableLoadingCache<>("sonarlint-filesystem", this::initializeFileSystem); public ClientFileSystemService(SonarLintRpcClient rpcClient, ApplicationEventPublisher eventPublisher, OpenFilesRepository openFilesRepository, TelemetryService telemetryService) { this.rpcClient = rpcClient; this.eventPublisher = eventPublisher; this.openFilesRepository = openFilesRepository; this.telemetryService = telemetryService; } public List getFiles(String configScopeId) { return List.copyOf(filesByConfigScopeIdCache.get(configScopeId).values()); } private static ClientFile fromDto(ClientFileDto clientFileDto) { var charset = charsetFromDto(clientFileDto.getCharset()); var forcedLanguage = clientFileDto.getDetectedLanguage(); var forcedSonarLanguage = forcedLanguage == null ? null : SonarLanguage.valueOf(forcedLanguage.name()); var file = new ClientFile(clientFileDto.getUri(), clientFileDto.getConfigScopeId(), clientFileDto.getIdeRelativePath(), clientFileDto.isTest(), charset, clientFileDto.getFsPath(), forcedSonarLanguage, clientFileDto.isUserDefined()); if (clientFileDto.getContent() != null) { file.setDirty(clientFileDto.getContent()); } return file; } @Nullable private static Charset charsetFromDto(@Nullable String dtoCharset) { if (dtoCharset == null) { return null; } try { return Charset.forName(dtoCharset); } catch (Exception e) { return null; } } public List findFilesByNamesInScope(String configScopeId, List filenames) { return getFiles(configScopeId).stream() .filter(f -> filenames.contains(f.getClientRelativePath().getFileName().toString())) .toList(); } public List findSonarlintConfigurationFilesByScope(String configScopeId) { return getFiles(configScopeId).stream() .filter(ClientFile::isSonarlintConfigurationFile) .toList(); } public Map initializeFileSystem(String configScopeId, SonarLintCancelMonitor cancelMonitor) { var result = new ConcurrentHashMap(); var files = getClientFileDtos(configScopeId, cancelMonitor); files.forEach(clientFileDto -> { var clientFile = fromDto(clientFileDto); filesByUri.put(clientFileDto.getUri(), clientFile); result.put(clientFileDto.getUri(), clientFile); }); return result; } private List getClientFileDtos(String configScopeId, SonarLintCancelMonitor cancelMonitor) { var startTime = System.currentTimeMillis(); var future = rpcClient.listFiles(new ListFilesParams(configScopeId)); cancelMonitor.onCancel(() -> future.cancel(true)); var files = future.join().getFiles(); var endTime = System.currentTimeMillis(); telemetryService.updateListFilesPerformance(files.size(), endTime - startTime); return files; } public void didUpdateFileSystem(DidUpdateFileSystemParams params) { var removed = new ArrayList(); params.getRemovedFiles().forEach(uri -> { var clientFile = filesByUri.remove(uri); if (clientFile != null) { filesByConfigScopeIdCache.get(clientFile.getConfigScopeId()).remove(uri); removed.add(clientFile); } }); var added = new ArrayList(); params.getAddedFiles().forEach(clientFileDto -> { var clientFile = fromDto(clientFileDto); var previousFile = filesByUri.put(clientFileDto.getUri(), clientFile); // We only send send the ADDED event for files that were actually added (not existing before) if (previousFile == null) { added.add(clientFile); } var byScope = filesByConfigScopeIdCache.get(clientFileDto.getConfigScopeId()); byScope.put(clientFileDto.getUri(), clientFile); }); var updated = new ArrayList(); params.getChangedFiles().forEach(clientFileDto -> { var clientFile = fromDto(clientFileDto); var previousFile = filesByUri.put(clientFileDto.getUri(), clientFile); // Modifying an unknown file is equals to adding it if (previousFile != null) { updated.add(clientFile); } else { added.add(clientFile); } var byScope = filesByConfigScopeIdCache.get(clientFileDto.getConfigScopeId()); byScope.put(clientFileDto.getUri(), clientFile); }); eventPublisher.publishEvent(new FileSystemUpdatedEvent(removed, added, updated)); } @EventListener public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) { var removedFilesByURI = filesByConfigScopeIdCache.get(event.getRemovedConfigurationScopeId()); filesByConfigScopeIdCache.clear(event.getRemovedConfigurationScopeId()); if (removedFilesByURI != null) { removedFilesByURI.keySet().forEach(filesByUri::remove); } } @PreDestroy public void shutdown() { filesByConfigScopeIdCache.close(); } /** * This will trigger loading the FS from the client if needed */ @CheckForNull public ClientFile getClientFiles(String configScopeId, URI fileUri) { return filesByConfigScopeIdCache.get(configScopeId).get(fileUri); } /** * This will NOT trigger loading the FS from the client */ @CheckForNull public ClientFile getClientFile(URI fileUri) { return filesByUri.get(fileUri); } @CheckForNull public Path getBaseDir(String configurationScopeId) { return baseDirPerConfigScopeId.computeIfAbsent(configurationScopeId, k -> { try { return rpcClient.getBaseDir(new GetBaseDirParams(configurationScopeId)).join().getBaseDir(); } catch (Exception e) { LOG.error("Error when getting the base dir from the client", e); return null; } }); } public void didOpenFile(String configurationScopeId, URI fileUri) { var isNewlyOpenedFile = openFilesRepository.considerOpened(configurationScopeId, fileUri); if (isNewlyOpenedFile) { eventPublisher.publishEvent(new FileOpenedEvent(configurationScopeId, fileUri)); } } public void didCloseFile(String configurationScopeId, URI fileUri) { openFilesRepository.considerClosed(configurationScopeId, fileUri); } public Map> groupFilesByConfigScope(Set fileUris) { return fileUris.stream() .map(filesByUri::get) .filter(Objects::nonNull) .collect(Collectors.groupingBy( ClientFile::getConfigScopeId, Collectors.mapping( ClientFile::getUri, Collectors.toSet() ) )); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.net.URI; import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.CoreProperties; import org.sonar.api.batch.fs.InputFile; import org.sonarsource.sonarlint.core.ServerFileExclusions; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; import org.sonarsource.sonarlint.core.commons.SmartCancelableLoadingCache; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FileUtils; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.file.WindowsShortcutUtils; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.FileStatusDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetFileExclusionsParams; import org.sonarsource.sonarlint.core.serverconnection.AnalyzerConfiguration; import org.sonarsource.sonarlint.core.serverconnection.IssueStorePaths; import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent; import org.sonarsource.sonarlint.core.serverconnection.storage.StorageException; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.event.EventListener; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.commons.util.git.GitService.createSonarLintGitIgnore; public class FileExclusionService { private static final SonarLintLogger LOG = SonarLintLogger.get(); // 5 MB private static final long MAX_AUTO_ANALYSIS_FILE_SIZE_BYTES = 5L * 1024 * 1024; // See org.sonar.api.scan.filesystem.FileExclusions private static final Set ALL_EXCLUSION_RELATED_SETTINGS = Set.of( CoreProperties.PROJECT_INCLUSIONS_PROPERTY, CoreProperties.PROJECT_TEST_INCLUSIONS_PROPERTY, CoreProperties.GLOBAL_EXCLUSIONS_PROPERTY, CoreProperties.PROJECT_EXCLUSIONS_PROPERTY, CoreProperties.GLOBAL_TEST_EXCLUSIONS_PROPERTY, CoreProperties.PROJECT_TEST_EXCLUSIONS_PROPERTY); private final ConfigurationRepository configRepo; private final StorageService storageService; private final PathTranslationService pathTranslationService; private final ClientFileSystemService clientFileSystemService; private final SonarLintRpcClient client; private final SmartCancelableLoadingCache serverExclusionByUriCache = new SmartCancelableLoadingCache<>("sonarlint-file-exclusions", this::computeIfExcluded, (key, oldValue, newValue) -> {}); public FileExclusionService(ConfigurationRepository configRepo, StorageService storageService, PathTranslationService pathTranslationService, ClientFileSystemService clientFileSystemService, SonarLintRpcClient client) { this.configRepo = configRepo; this.storageService = storageService; this.pathTranslationService = pathTranslationService; this.clientFileSystemService = clientFileSystemService; this.client = client; } public boolean computeIfExcluded(URI fileUri, SonarLintCancelMonitor cancelMonitor) { LOG.debug("Computing file exclusion for uri '{}'", fileUri); var clientFile = clientFileSystemService.getClientFile(fileUri); if (clientFile == null) { LOG.debug("Unable to find client file for uri {}", fileUri); return false; } var configScope = clientFile.getConfigScopeId(); var effectiveBindingOpt = configRepo.getEffectiveBinding(configScope); if (effectiveBindingOpt.isEmpty()) { return false; } var analyzerStorage = storageService.connection(effectiveBindingOpt.get().connectionId()) .project(effectiveBindingOpt.get().sonarProjectKey()) .analyzerConfiguration(); if (!analyzerStorage.isValid()) { LOG.warn("Unable to read settings in local storage, analysis storage is not ready"); return false; } AnalyzerConfiguration analyzerConfig; try { analyzerConfig = analyzerStorage.read(); } catch (StorageException e) { LOG.debug("Unable to read settings in local storage", e); return false; } var settings = new MapSettings(analyzerConfig.getSettings().getAll()); var exclusionFilters = new ServerFileExclusions(settings.asConfig()); exclusionFilters.prepare(); var idePath = clientFile.getClientRelativePath(); var pathTranslation = pathTranslationService.getOrComputePathTranslation(configScope); Path serverPath; if (pathTranslation.isPresent()) { serverPath = IssueStorePaths.idePathToServerPath(pathTranslation.get().getIdePathPrefix(), pathTranslation.get().getServerPathPrefix(), idePath); if (serverPath == null) { // we can't map it to a Sonar server path, so just apply exclusions to the original ide path serverPath = idePath; } } else { serverPath = idePath; } var type = clientFile.isTest() ? InputFile.Type.TEST : InputFile.Type.MAIN; var result = !exclusionFilters.accept(serverPath.toString(), type); LOG.debug("File exclusion for uri '{}' is {}", fileUri, result); return result; } @EventListener public void onBindingChanged(BindingConfigChangedEvent event) { if (event.newConfig().isBound()) { var connectionId = requireNonNull(event.newConfig().connectionId()); var projectKey = requireNonNull(event.newConfig().sonarProjectKey()); // do not recompute exclusions if storage does not yet contain settings (will be done by onFileExclusionSettingsChanged later) if (storageService.connection(connectionId).project(projectKey).analyzerConfiguration().isValid()) { LOG.debug("Binding changed for config scope '{}', recompute file exclusions...", event.configScopeId()); forEachFileInScopeAndInheritedDescendants(event.configScopeId(), f -> serverExclusionByUriCache.refreshAsync(f.getUri())); } } else { LOG.debug("Binding removed for config scope '{}', clearing file exclusions...", event.configScopeId()); forEachFileInScopeAndInheritedDescendants(event.configScopeId(), f -> serverExclusionByUriCache.clear(f.getUri())); } } private void forEachFileInScopeAndInheritedDescendants(String rootScopeId, Consumer action) { Stream.concat(Stream.of(rootScopeId), configRepo.getChildrenWithInheritedBinding(rootScopeId).stream()) .forEach(scopeId -> clientFileSystemService.getFiles(scopeId).forEach(action)); } @EventListener public void onFileSystemUpdated(FileSystemUpdatedEvent event) { event.getRemoved().forEach(f -> serverExclusionByUriCache.clear(f.getUri())); // We could try to be more efficient by looking at changed files, and deciding if we need to invalidate or not based on changed // attributes (relative path, isTest). But it's probably not worth the effort. Stream.concat(event.getAdded().stream(), event.getUpdated().stream()) .forEach(f -> serverExclusionByUriCache.refreshAsync(f.getUri())); } @EventListener public void onFileExclusionSettingsChanged(SonarServerSettingsChangedEvent event) { var settingsDiff = event.updatedSettingsValueByKey(); if (isFileExclusionSettingsDifferent(settingsDiff)) { LOG.debug("File exclusion settings changed, recompute all file exclusions..."); event.configScopeIds().forEach(configScopeId -> clientFileSystemService.getFiles(configScopeId) .forEach(f -> serverExclusionByUriCache.refreshAsync(f.getUri()))); } } private static boolean isFileExclusionSettingsDifferent(Map updatedSettingsValueByKey) { return ALL_EXCLUSION_RELATED_SETTINGS.stream().anyMatch(updatedSettingsValueByKey::containsKey); } public Map getFilesStatus(Map> fileUrisByConfigScope) { var result = new HashMap(); for (var entry : fileUrisByConfigScope.entrySet()) { var configScopeId = entry.getKey(); var baseDir = clientFileSystemService.getBaseDir(configScopeId); var files = new HashSet<>(entry.getValue()); var filteredFileUris = filterOutExcludedFiles(configScopeId, baseDir, files).stream().map(ClientFile::getUri).collect(Collectors.toSet()); files.forEach(uri -> result.put(uri, new FileStatusDto(!filteredFileUris.contains(uri)))); } return result; } public boolean isExcludedFromServer(URI fileUri) { return Boolean.TRUE.equals(serverExclusionByUriCache.get(fileUri)); } public List filterOutExcludedFiles(String configurationScopeId, @Nullable Path baseDir, Set files) { var sonarLintGitIgnore = createSonarLintGitIgnore(baseDir); // INFO: When there are additional filters coming at some point, add them here and log them down below as well! var filteredURIsFromServerExclusionService = new ArrayList(); var filteredURIsFromGitIgnore = new ArrayList(); var filteredURIsNotUserDefined = new ArrayList(); var filteredURIsFromSymbolicLink = new ArrayList(); var filteredURIsFromWindowsShortcut = new ArrayList(); var filteredURIsNoFile = new ArrayList(); var filteredURIsTooLarge = new ArrayList(); var filesToExclude = files; if (configRepo.getEffectiveBinding(configurationScopeId).isEmpty()) { // client-defined file exclusions only apply in standalone mode filesToExclude = filterOutClientExcludedFiles(configurationScopeId, files); } // Do the actual filtering and in case of a filtered out URI, save them for later logging! var actualFilesToAnalyze = filesToExclude .stream() .map(uri -> { var file = findFile(configurationScopeId, uri); if (file == null) { filteredURIsNoFile.add(uri); } return file; }) .filter(Objects::nonNull) .filter(file -> { if (isExcludedFromServer(file.getUri())) { filteredURIsFromServerExclusionService.add(file.getUri()); return false; } return true; }) .filter(file -> { if (sonarLintGitIgnore.isFileIgnored(file.getClientRelativePath())) { filteredURIsFromGitIgnore.add(file.getUri()); return false; } return true; }) .filter(file -> { if (!file.isUserDefined()) { filteredURIsNotUserDefined.add(file.getUri()); return false; } return true; }) .filter(file -> { try { if (file.isLargerThan(MAX_AUTO_ANALYSIS_FILE_SIZE_BYTES)) { filteredURIsTooLarge.add(file.getUri()); return false; } } catch (Exception e) { // Could not determine the size, include the file by default } return true; }) .filter(file -> { // On Schemes like "temp" (used by IntelliJ) or "rse" (Eclipse Remote System Explorer), // the check for a symbolic link or Windows shortcut will fail as these file systems cannot be resolved for the operations. // If this happens, we won't exclude the file as the chance for someone to use a protocol with such a scheme while also using // symbolic links or Windows shortcuts should be near zero, and this is less error-prone than excluding the try { var uri = file.getUri(); if (Files.isSymbolicLink(FileUtils.getFilePathFromUri(uri))) { filteredURIsFromSymbolicLink.add(uri); return false; } else if (WindowsShortcutUtils.isWindowsShortcut(uri)) { filteredURIsFromWindowsShortcut.add(uri); return false; } return true; } catch (FileSystemNotFoundException err) { LOG.debug("Checking for symbolic links or Windows shortcuts in the file system is not possible for the URI '" + file + "'. Therefore skipping the checks due to the underlying protocol / its scheme.", err); return true; } }) .toList(); // Log all the filtered out URIs but not for the filters where there were none logFilteredURIs("Filtered out URIs based on the server exclusion service", filteredURIsFromServerExclusionService); logFilteredURIs("Filtered out URIs ignored by Git", filteredURIsFromGitIgnore); logFilteredURIs("Filtered out URIs not user-defined", filteredURIsNotUserDefined); logFilteredURIs("Filtered out URIs exceeding max allowed size", filteredURIsTooLarge); logFilteredURIs("Filtered out URIs that are symbolic links", filteredURIsFromSymbolicLink); logFilteredURIs("Filtered out URIs that are Windows shortcuts", filteredURIsFromWindowsShortcut); logFilteredURIs("Filtered out URIs having no file", filteredURIsNoFile); return actualFilesToAnalyze; } @CheckForNull private ClientFile findFile(String configScopeId, URI fileUriToAnalyze) { var clientFile = clientFileSystemService.getClientFiles(configScopeId, fileUriToAnalyze); if (clientFile == null) { LOG.error("File to analyze was not found in the file system: {}", fileUriToAnalyze); return null; } return clientFile; } private void logFilteredURIs(String reason, ArrayList uris) { if (!uris.isEmpty()) { SonarLintLogger.get().debug(reason + ": " + String.join(", ", uris.stream().map(Object::toString).toList())); } } private Set filterOutClientExcludedFiles(String configurationScopeId, Set files) { var fileExclusionsGlobPatterns = getClientFileExclusionPatterns(configurationScopeId); var matchers = parseGlobPatterns(fileExclusionsGlobPatterns); Predicate fileExclusionFilter = uri -> matchers.stream().noneMatch(matcher -> matcher.matches(Paths.get(uri))); return files.stream() .filter(fileExclusionFilter) .collect(Collectors.toSet()); } private Set getClientFileExclusionPatterns(String configurationScopeId) { try { return client.getFileExclusions(new GetFileExclusionsParams(configurationScopeId)).join().getFileExclusionPatterns(); } catch (Exception e) { LOG.error("Error when requesting the file exclusions", e); return Collections.emptySet(); } } private static List parseGlobPatterns(Set globPatterns) { var fs = FileSystems.getDefault(); List parsedMatchers = new ArrayList<>(globPatterns.size()); for (String pattern : globPatterns) { try { parsedMatchers.add(fs.getPathMatcher("glob:" + pattern)); } catch (Exception e) { // ignore invalid patterns, skip them } } return parsedMatchers; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileOpenedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.net.URI; public record FileOpenedEvent(String configurationScopeId, URI fileUri) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileSystemUpdatedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.util.List; import java.util.stream.Stream; public class FileSystemUpdatedEvent { private final List removed; private final List added; private final List updated; public FileSystemUpdatedEvent(List removed, List added, List updated) { this.removed = removed; this.added = added; this.updated = updated; } public List getRemoved() { return removed; } public List getAdded() { return added; } public List getUpdated() { return updated; } public List getAddedOrUpdated() { return Stream.concat(getAdded().stream(), getUpdated().stream()) .toList(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/OpenFilesRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.net.URI; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; public class OpenFilesRepository { private final Map> openFilesByConfigScopeId = new ConcurrentHashMap<>(); /** * @return true if the file was previously not considered open; it is a newly opened file */ public boolean considerOpened(String configurationScopeId, URI fileUri) { var openFiles = openFilesByConfigScopeId.computeIfAbsent(configurationScopeId, k -> new HashSet<>()); return openFiles.add(fileUri); } public void considerClosed(String configurationScopeId, URI fileUri) { var openFiles = openFilesByConfigScopeId.get(configurationScopeId); if (openFiles != null) { openFiles.remove(fileUri); } } public Set getOpenFilesAmong(String configurationScopeId, Set fileUris) { var openFiles = openFilesByConfigScopeId.getOrDefault(configurationScopeId, Set.of()); return openFiles.stream().filter(fileUris::contains).collect(Collectors.toSet()); } public Map> getOpenFilesByConfigScopeId() { return openFilesByConfigScopeId; } public Set getOpenFilesForConfigScope(String configurationScopeId) { return openFilesByConfigScopeId.getOrDefault(configurationScopeId, Set.of()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.fs; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.hotspot; import java.util.List; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckLocalDetectionSupportedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckStatusChangePermittedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.OpenUrlInBrowserParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotChangedEvent; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotClosedEvent; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotRaisedEvent; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import org.springframework.context.event.EventListener; public class HotspotService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String NO_BINDING_REASON = "The project is not bound, please bind it to SonarQube (Server, Cloud)"; private static final String REVIEW_STATUS_UPDATE_PERMISSION_MISSING_REASON = "Changing a hotspot's status requires the 'Administer Security Hotspot' permission."; private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionRepository; private final SonarQubeClientManager sonarQubeClientManager; private final TelemetryService telemetryService; private final SonarProjectBranchTrackingService branchTrackingService; private final FindingReportingService findingReportingService; private final StorageService storageService; public HotspotService(SonarLintRpcClient client, StorageService storageService, ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionRepository, SonarQubeClientManager sonarQubeClientManager, TelemetryService telemetryService, SonarProjectBranchTrackingService branchTrackingService, FindingReportingService findingReportingService) { this.client = client; this.storageService = storageService; this.configurationRepository = configurationRepository; this.connectionRepository = connectionRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.telemetryService = telemetryService; this.branchTrackingService = branchTrackingService; this.findingReportingService = findingReportingService; } public void openHotspotInBrowser(String configScopeId, String hotspotKey) { var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); var endpointParams = effectiveBinding.flatMap(binding -> connectionRepository.getEndpointParams(binding.connectionId())); if (effectiveBinding.isEmpty() || endpointParams.isEmpty()) { LOG.warn("Configuration scope {} is not bound properly, unable to open hotspot", configScopeId); return; } var branchName = branchTrackingService.awaitEffectiveSonarProjectBranch(configScopeId); if (branchName.isEmpty()) { LOG.warn("Configuration scope {} has no matching branch, unable to open hotspot", configScopeId); return; } var url = buildHotspotUrl(effectiveBinding.get().sonarProjectKey(), branchName.get(), hotspotKey, endpointParams.get()); client.openUrlInBrowser(new OpenUrlInBrowserParams(url)); telemetryService.hotspotOpenedInBrowser(); } public CheckLocalDetectionSupportedResponse checkLocalDetectionSupported(String configScopeId) { var configScope = configurationRepository.getConfigurationScope(configScopeId); if (configScope == null) { var error = new ResponseError(SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_FOUND, "The provided configuration scope does not exist: " + configScopeId, configScopeId); throw new ResponseErrorException(error); } var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isEmpty()) { return new CheckLocalDetectionSupportedResponse(false, NO_BINDING_REASON); } var connectionId = effectiveBinding.get().connectionId(); if (connectionRepository.getConnectionById(connectionId) == null) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection: " + connectionId, connectionId); throw new ResponseErrorException(error); } return new CheckLocalDetectionSupportedResponse(true, null); } public CheckStatusChangePermittedResponse checkStatusChangePermitted(String connectionId, String hotspotKey, SonarLintCancelMonitor cancelMonitor) { // fixme add getConnectionByIdOrThrow var connection = connectionRepository.getConnectionById(connectionId); var r = sonarQubeClientManager.getValidClientOrThrow(connectionId) .withClientApiAndReturn(serverApi -> serverApi.hotspot().show(hotspotKey, cancelMonitor)); var allowedStatuses = HotspotReviewStatus.allowedStatusesOn(connection.getKind()); // canChangeStatus is false when the 'Administer Hotspots' permission is missing // normally the 'Browse' permission is also required, but we assume it's present as the client knows the hotspot key return toResponse(r.canChangeStatus, allowedStatuses); } private static CheckStatusChangePermittedResponse toResponse(boolean canChangeStatus, List coreStatuses) { return new CheckStatusChangePermittedResponse(canChangeStatus, canChangeStatus ? null : REVIEW_STATUS_UPDATE_PERMISSION_MISSING_REASON, coreStatuses.stream().map(s -> HotspotStatus.valueOf(s.name())) // respect ordering of the client-api enum for the UI .sorted() .toList()); } public void changeStatus(String configurationScopeId, String hotspotKey, HotspotReviewStatus newStatus, SonarLintCancelMonitor cancelMonitor) { var effectiveBindingOpt = configurationRepository.getEffectiveBinding(configurationScopeId); if (effectiveBindingOpt.isEmpty()) { LOG.debug("No binding for config scope {}", configurationScopeId); return; } sonarQubeClientManager.withActiveClient(effectiveBindingOpt.get().connectionId(), serverApi -> { serverApi.hotspot().changeStatus(hotspotKey, newStatus, cancelMonitor); saveStatusInStorage(effectiveBindingOpt.get(), hotspotKey, newStatus); telemetryService.hotspotStatusChanged(); }); } private void saveStatusInStorage(Binding binding, String hotspotKey, HotspotReviewStatus newStatus) { storageService.binding(binding) .findings() .changeHotspotStatus(hotspotKey, newStatus); } static String buildHotspotUrl(String projectKey, String branch, String hotspotKey, EndpointParams endpointParams) { var relativePath = (endpointParams.isSonarCloud() ? "/project/security_hotspots?id=" : "/security_hotspots?id=") + UrlUtils.urlEncode(projectKey) + "&branch=" + UrlUtils.urlEncode(branch) + "&hotspots=" + UrlUtils.urlEncode(hotspotKey); return ServerApiHelper.concat(endpointParams.getBaseUrl(), relativePath); } @EventListener public void onServerEventReceived(SonarServerEventReceivedEvent event) { var connectionId = event.getConnectionId(); var serverEvent = event.getEvent(); if (serverEvent instanceof SecurityHotspotChangedEvent hotspotChangedEvent) { updateStorage(connectionId, hotspotChangedEvent); republishPreviouslyRaisedHotspots(connectionId, hotspotChangedEvent); } else if (serverEvent instanceof SecurityHotspotClosedEvent hotspotClosedEvent) { updateStorage(connectionId, hotspotClosedEvent); republishPreviouslyRaisedHotspots(connectionId, hotspotClosedEvent); } else if (serverEvent instanceof SecurityHotspotRaisedEvent hotspotRaisedEvent) { // We could try to match with an existing hotspot. But we don't do it because we don't invest in hotspots right now. updateStorage(connectionId, hotspotRaisedEvent); } } private void updateStorage(String connectionId, SecurityHotspotRaisedEvent event) { var hotspot = new ServerHotspot( event.getHotspotKey(), event.getRuleKey(), event.getMainLocation().getMessage(), event.getMainLocation().getFilePath(), TaintVulnerabilityTrackingService.adapt(event.getMainLocation().getTextRange()), event.getCreationDate(), event.getStatus(), event.getVulnerabilityProbability(), null); var projectKey = event.getProjectKey(); storageService.connection(connectionId).project(projectKey).findings().insert(event.getBranch(), hotspot); } private void updateStorage(String connectionId, SecurityHotspotClosedEvent event) { var projectKey = event.getProjectKey(); storageService.connection(connectionId).project(projectKey).findings().deleteHotspot(event.getHotspotKey()); } private void updateStorage(String connectionId, SecurityHotspotChangedEvent event) { var projectKey = event.getProjectKey(); storageService.connection(connectionId).project(projectKey).findings().updateHotspot(event.getHotspotKey(), hotspot -> { var status = event.getStatus(); if (status != null) { hotspot.setStatus(status); } var assignee = event.getAssignee(); if (assignee != null) { hotspot.setAssignee(assignee); } }); } private void republishPreviouslyRaisedHotspots(String connectionId, SecurityHotspotChangedEvent event) { var boundScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, event.getProjectKey()); boundScopes.forEach(scope -> { var scopeId = scope.getConfigScopeId(); findingReportingService.updateAndReportHotspots(scopeId, raisedHotspotDto -> changedHotspotUpdater(raisedHotspotDto, event)); }); } private static RaisedHotspotDto changedHotspotUpdater(RaisedHotspotDto raisedHotspotDto, SecurityHotspotChangedEvent event) { if (event.getHotspotKey().equals(raisedHotspotDto.getServerKey())) { return raisedHotspotDto.withHotspotStatusAndResolution(HotspotStatus.valueOf(event.getStatus().name()), event.getStatus().isResolved()); } return raisedHotspotDto; } private void republishPreviouslyRaisedHotspots(String connectionId, SecurityHotspotClosedEvent event) { var boundScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, event.getProjectKey()); boundScopes.forEach(scope -> { var scopeId = scope.getConfigScopeId(); findingReportingService.updateAndReportHotspots(scopeId, raisedHotspotDto -> closedHotspotUpdater(raisedHotspotDto, event)); }); } private static RaisedHotspotDto closedHotspotUpdater(RaisedHotspotDto raisedHotspotDto, SecurityHotspotClosedEvent event) { if (event.getHotspotKey().equals(raisedHotspotDto.getServerKey())) { return null; } return raisedHotspotDto; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/HotspotStatusChangeException.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.hotspot; public class HotspotStatusChangeException extends RuntimeException { public HotspotStatusChangeException(Throwable cause) { super("Cannot change status on the hotspot", cause); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/hotspot/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.hotspot; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/http/AskClientCertificatePredicate.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.util.Arrays; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import nl.altindag.ssl.model.TrustManagerParameters; import nl.altindag.ssl.util.CertificateUtils; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.CheckServerTrustedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.X509CertificateDto; public class AskClientCertificatePredicate implements Predicate { private final SonarLintRpcClient client; public AskClientCertificatePredicate(SonarLintRpcClient client) { this.client = client; } @Override public boolean test(TrustManagerParameters trustManagerParameters) { try { return client .checkServerTrusted(new CheckServerTrustedParams( Arrays.stream(trustManagerParameters.getChain()) .map(c -> new X509CertificateDto(CertificateUtils.convertToPem(c))).toList(), trustManagerParameters.getAuthType())) .get() .isTrusted(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return false; } catch (ExecutionException ex) { throw new RuntimeException(ex); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/http/ClientProxyCredentialsProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.protocol.HttpContext; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationParams; /** * Inspired by {@link org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider} but asking client instead of * asking JDK */ public class ClientProxyCredentialsProvider implements CredentialsProvider { private final SonarLintLogger logger = SonarLintLogger.get(); private final SonarLintRpcClient client; public ClientProxyCredentialsProvider(SonarLintRpcClient client) { this.client = client; } @Override public Credentials getCredentials(AuthScope authScope, @Nullable HttpContext context) { var host = authScope.getHost(); if (host == null || context == null) { return null; } try { var targetHostURI = HttpClientContext.adapt(context).getRequest().getUri(); var protocol = getProtocol(authScope); var response = client.getProxyPasswordAuthentication( new GetProxyPasswordAuthenticationParams(host, authScope.getPort(), protocol, authScope.getRealm(), authScope.getSchemeName(), targetHostURI.toURL())) .get(); var proxyUser = response.getProxyUser(); if (proxyUser != null) { var proxyPassword = response.getProxyPassword(); return new UsernamePasswordCredentials(proxyUser, proxyPassword != null ? proxyPassword.toCharArray() : null); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.warn("Interrupted!", e); } catch (URISyntaxException | MalformedURLException | ExecutionException e) { logger.warn("Unable to get proxy credentials from the client", e); } return null; } private static String getProtocol(AuthScope authScope) { String protocol; if (authScope.getProtocol() != null) { protocol = authScope.getProtocol(); } else { protocol = (authScope.getPort() == 443 ? URIScheme.HTTPS.id : URIScheme.HTTP.id); } return protocol; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/http/ClientProxySelector.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.SocketAddress; import java.net.URI; import java.util.List; import java.util.concurrent.ExecutionException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.SelectProxiesParams; public class ClientProxySelector extends ProxySelector { private final SonarLintLogger logger = SonarLintLogger.get(); private final SonarLintRpcClient client; public ClientProxySelector(SonarLintRpcClient client) { this.client = client; } @Override public List select(URI uri) { try { return client.selectProxies(new SelectProxiesParams(uri)).get().getProxies().stream() .map(p -> { if (p.getType() == Proxy.Type.DIRECT) { return Proxy.NO_PROXY; } else { if (p.getPort() < 0 || p.getPort() > 65535) { throw new IllegalStateException("Port is outside the valid range for hostname: " + p.getHostname()); } return new Proxy(p.getType(), new InetSocketAddress(p.getHostname(), p.getPort())); } }) .toList(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.warn("Interrupted!", e); } catch (IllegalStateException | ExecutionException e) { logger.warn("Unable to get proxy", e); } return List.of(); } @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { // Intentionally left blank: connection failures are handled by the HTTP client, no additional action required here } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/http/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.http; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueNotFoundException.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.issue; import java.util.UUID; public class IssueNotFoundException extends Exception { private final UUID issueKey; public IssueNotFoundException(String message, UUID issueKey) { super(message); this.issueKey = issueKey; } public UUID getIssueKey() { return issueKey; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/IssueService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.issue; import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.Transition; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.LocalOnlyIssueStatusChangedEvent; import org.sonarsource.sonarlint.core.event.ServerIssueStatusChangedEvent; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.local.only.XodusLocalOnlyIssueStorageService; import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.newcode.NewCodeService; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.CheckStatusChangePermittedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.EffectiveIssueDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenAllIssuesForFileParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rules.RuleDetails; import org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter; import org.sonarsource.sonarlint.core.rules.RuleNotFoundException; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.push.IssueChangedEvent; import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.issues.LocalOnlyIssuesRepository; import org.sonarsource.sonarlint.core.serverconnection.storage.ProjectServerIssueStore; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.tracking.LocalOnlyIssueRepository; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; public class IssueService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String STATUS_CHANGE_PERMISSION_MISSING_REASON = "Marking an issue as resolved requires the 'Administer Issues' permission"; private static final String UNSUPPORTED_SQ_VERSION_REASON = "Marking a local-only issue as resolved requires SonarQube Server 10.2+"; private static final Version SQ_ANTICIPATED_TRANSITIONS_MIN_VERSION = Version.create("10.2"); /** * With SQ 10.4 the transitions changed from "Won't fix" to "Accept" */ private static final Version SQ_ACCEPTED_TRANSITION_MIN_VERSION = Version.create("10.4"); private static final List NEW_RESOLUTION_STATUSES = List.of(ResolutionStatus.ACCEPT, ResolutionStatus.FALSE_POSITIVE); private static final List OLD_RESOLUTION_STATUSES = List.of(ResolutionStatus.WONT_FIX, ResolutionStatus.FALSE_POSITIVE); private static final Map transitionByResolutionStatus = Map.of( ResolutionStatus.ACCEPT, Transition.ACCEPT, ResolutionStatus.WONT_FIX, Transition.WONT_FIX, ResolutionStatus.FALSE_POSITIVE, Transition.FALSE_POSITIVE); private final ConfigurationRepository configurationRepository; private final SonarQubeClientManager sonarQubeClientManager; private final StorageService storageService; private final XodusLocalOnlyIssueStorageService localOnlyIssueStorageService; private final LocalOnlyIssueRepository localOnlyIssueRepository; private final ApplicationEventPublisher eventPublisher; private final FindingReportingService findingReportingService; private final SeverityModeService severityModeService; private final NewCodeService newCodeService; private final ActiveRulesService activeRulesService; private final TaintVulnerabilityTrackingService taintVulnerabilityTrackingService; private final AiCodeFixService aiCodeFixService; private final LocalOnlyIssuesRepository localOnlyIssuesRepository; public IssueService(ConfigurationRepository configurationRepository, SonarQubeClientManager sonarQubeClientManager, StorageService storageService, XodusLocalOnlyIssueStorageService localOnlyIssueStorageService, LocalOnlyIssueRepository localOnlyIssueRepository, ApplicationEventPublisher eventPublisher, FindingReportingService findingReportingService, SeverityModeService severityModeService, NewCodeService newCodeService, ActiveRulesService activeRulesService, TaintVulnerabilityTrackingService taintVulnerabilityTrackingService, AiCodeFixService aiCodeFixService, LocalOnlyIssuesRepository localOnlyIssuesRepository) { this.configurationRepository = configurationRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.storageService = storageService; this.localOnlyIssueStorageService = localOnlyIssueStorageService; this.localOnlyIssueRepository = localOnlyIssueRepository; this.eventPublisher = eventPublisher; this.findingReportingService = findingReportingService; this.severityModeService = severityModeService; this.newCodeService = newCodeService; this.activeRulesService = activeRulesService; this.taintVulnerabilityTrackingService = taintVulnerabilityTrackingService; this.aiCodeFixService = aiCodeFixService; this.localOnlyIssuesRepository = localOnlyIssuesRepository; } @PostConstruct public void migrateData() { if (localOnlyIssueStorageService.exists()) { try { LOG.info("Migrating the Xodus local-only issues to H2"); var migrationStart = System.currentTimeMillis(); var xodusLocalOnlyIssueStore = localOnlyIssueStorageService.get(); var issuesPerConfigScope = xodusLocalOnlyIssueStore.loadAll(); localOnlyIssuesRepository.storeIssues(issuesPerConfigScope); LOG.info("Migrated Xodus local-only issues to H2, took {}ms", System.currentTimeMillis() - migrationStart); } catch (Exception e) { LOG.error("Unable to migrate local-only findings, will use fresh DB", e); } } // always call to remove lingering temporary files localOnlyIssueStorageService.delete(); } public void changeStatus(String configurationScopeId, String issueKey, ResolutionStatus newStatus, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); var serverConnection = sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()); var reviewStatus = transitionByResolutionStatus.get(newStatus); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueKey); if (isServerIssue) { serverConnection.withClientApi(serverApi -> serverApi.issue().changeStatus(issueKey, reviewStatus, cancelMonitor)); projectServerIssueStore.updateIssueResolutionStatus(issueKey, isTaintIssue, true) .ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.connectionId(), binding.sonarProjectKey(), issue))); } else { var localIssueOpt = asUUID(issueKey).flatMap(localOnlyIssueRepository::findByKey); if (localIssueOpt.isEmpty()) { // this happens in case if VS client trying to change status of the issue for Roslyn analysed language // since analysis was executed outside the backend on the client side we trust client to provide valid issue key try { serverConnection.withClientApi(serverApi -> serverApi.issue().changeStatus(issueKey, reviewStatus, cancelMonitor)); return; } catch (NotFoundException ex) { throw issueNotFoundException(issueKey); } } var coreStatus = org.sonarsource.sonarlint.core.commons.IssueStatus.valueOf(newStatus.name()); var issue = localIssueOpt.get(); issue.resolve(coreStatus); var allIssues = localOnlyIssuesRepository.loadAll(configurationScopeId); serverConnection.withClientApi(serverApi -> serverApi.issue() .anticipatedTransitions(binding.sonarProjectKey(), concat(allIssues, issue), cancelMonitor)); localOnlyIssuesRepository.storeLocalOnlyIssue(configurationScopeId, issue); eventPublisher.publishEvent(new LocalOnlyIssueStatusChangedEvent(issue)); } } private static List concat(List issues, LocalOnlyIssue issue) { return Stream.concat(issues.stream(), Stream.of(issue)).toList(); } private static List subtract(List allIssues, List issueToSubtract) { return allIssues.stream() .filter(it -> issueToSubtract.stream().noneMatch(issue -> issue.getId().equals(it.getId()))) .toList(); } public boolean checkAnticipatedStatusChangeSupported(String configScopeId) { var binding = configurationRepository.getEffectiveBindingOrThrow(configScopeId); var connectionId = binding.connectionId(); return sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApiAndReturn(serverApi -> checkAnticipatedStatusChangeSupported(serverApi, connectionId)); } /** * Check if the anticipated transitions are supported on the server side (requires SonarQube 10.2+) * * @param api used for checking if server is a SonarQube instance * @param connectionId required to get the version information from the server * @return whether server is SonarQube instance and matches version requirement */ private boolean checkAnticipatedStatusChangeSupported(ServerApi api, String connectionId) { return !api.isSonarCloud() && storageService.connection(connectionId).serverInfo().read() .map(version -> version.version().satisfiesMinRequirement(SQ_ANTICIPATED_TRANSITIONS_MIN_VERSION)) .orElse(false); } public CheckStatusChangePermittedResponse checkStatusChangePermitted(String connectionId, String issueKey, SonarLintCancelMonitor cancelMonitor) { return sonarQubeClientManager.getValidClientOrThrow(connectionId).withClientApiAndReturn(serverApi -> asUUID(issueKey) .flatMap(localOnlyIssueRepository::findByKey) .map(r -> { // For anticipated issues we currently don't get the information from SonarQube (as there is no web API // endpoint) regarding the available transitions. SonarCloud doesn't provide it currently anyway. That's why we // have to rely on the version check for SonarQube (>= 10.2 / >=10.4) List statuses = List.of(); if (checkAnticipatedStatusChangeSupported(serverApi, connectionId)) { var is104orNewer = !serverApi.isSonarCloud() && is104orNewer(connectionId, serverApi, cancelMonitor); statuses = is104orNewer ? NEW_RESOLUTION_STATUSES : OLD_RESOLUTION_STATUSES; } return toResponse(statuses, UNSUPPORTED_SQ_VERSION_REASON); }) .orElseGet(() -> { var issue = serverApi.issue().searchByKey(issueKey, cancelMonitor); return toResponse(getAdministerIssueTransitions(issue), STATUS_CHANGE_PERMISSION_MISSING_REASON); })); } /** * For checking whether SonarQube is already on 10.4 or not. NEVER apply to SonarCloud as their version differs! */ private boolean is104orNewer(String connectionId, ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { var serverVersionSynchronizer = new ServerInfoSynchronizer(storageService.connection(connectionId)); var serverVersion = serverVersionSynchronizer.readOrSynchronizeServerInfo(serverApi, cancelMonitor); return serverVersion.version().compareToIgnoreQualifier(SQ_ACCEPTED_TRANSITION_MIN_VERSION) >= 0; } private static CheckStatusChangePermittedResponse toResponse(List statuses, String reason) { var permitted = !statuses.isEmpty(); // No status available means it is not permitted or not supported (e.g. SonarCloud for anticipated issues) return new CheckStatusChangePermittedResponse(permitted, permitted ? null : reason, statuses); } private static List getAdministerIssueTransitions(Issues.Issue issue) { // the 2 required transitions are not available when the 'Administer Issues' permission is missing // normally the 'Browse' permission is also required, but we assume it's present as the client knows the issue key var possibleTransitions = new HashSet<>(issue.getTransitions().getTransitionsList()); if (possibleTransitions.containsAll(toTransitionStatus(NEW_RESOLUTION_STATUSES))) { return NEW_RESOLUTION_STATUSES; } // No transitions meaning you're not allowed. That's it. return possibleTransitions.containsAll(toTransitionStatus(OLD_RESOLUTION_STATUSES)) ? OLD_RESOLUTION_STATUSES : List.of(); } private static Set toTransitionStatus(List resolutions) { return resolutions.stream() .map(resolution -> transitionByResolutionStatus.get(resolution).getStatus()) .collect(Collectors.toSet()); } public void addComment(String configurationScopeId, String issueKey, String text, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueKey); if (isServerIssue) { addCommentOnServerIssue(configurationScopeId, issueKey, text, cancelMonitor); } else { var optionalId = asUUID(issueKey); if (optionalId.isPresent()) { setCommentOnLocalOnlyIssue(configurationScopeId, optionalId.get(), text, cancelMonitor); } else { // this happens in case if VS client trying to add comment of the issue for Roslyn analysed language // since analysis was executed outside the backend on the client side we trust client to provide valid issue key try { addCommentOnServerIssue(configurationScopeId, issueKey, text, cancelMonitor); } catch (NotFoundException ex) { throw issueNotFoundException(issueKey); } } } } public boolean reopenIssue(String configurationScopeId, String issueId, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); var projectServerIssueStore = storageService.binding(binding).findings(); boolean isServerIssue = projectServerIssueStore.containsIssue(issueId); if (isServerIssue) { return sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApiAndReturn(serverApi -> reopenServerIssue(serverApi, binding, issueId, projectServerIssueStore, isTaintIssue, cancelMonitor)); } else { return reopenLocalIssue(issueId, configurationScopeId, cancelMonitor); } } public boolean reopenAllIssuesForFile(ReopenAllIssuesForFileParams params, SonarLintCancelMonitor cancelMonitor) { var configurationScopeId = params.getConfigurationScopeId(); var ideRelativePath = params.getIdeRelativePath(); var allIssues = localOnlyIssuesRepository.loadAll(configurationScopeId); var issuesForFile = localOnlyIssuesRepository.loadForFile(configurationScopeId, ideRelativePath); var issuesToSync = subtract(allIssues, issuesForFile); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); return localOnlyIssuesRepository.removeAllIssuesForFile(configurationScopeId, ideRelativePath); } private void removeIssueOnServer(String configurationScopeId, UUID issueId, SonarLintCancelMonitor cancelMonitor) { var allIssues = localOnlyIssuesRepository.loadAll(configurationScopeId); var issuesToSync = allIssues.stream().filter(it -> !it.getId().equals(issueId)).toList(); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); } private void setCommentOnLocalOnlyIssue(String configurationScopeId, UUID issueId, String comment, SonarLintCancelMonitor cancelMonitor) { var optionalLocalOnlyIssue = localOnlyIssuesRepository.find(issueId); if (optionalLocalOnlyIssue.isPresent()) { var commentedIssue = optionalLocalOnlyIssue.get(); var resolution = commentedIssue.getResolution(); if (resolution != null) { resolution.setComment(comment); var issuesToSync = new ArrayList<>(localOnlyIssuesRepository.loadAll(configurationScopeId)); issuesToSync.replaceAll(issue -> issue.getId().equals(issueId) ? commentedIssue : issue); var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApi(serverApi -> serverApi.issue().anticipatedTransitions(binding.sonarProjectKey(), issuesToSync, cancelMonitor)); localOnlyIssuesRepository.storeLocalOnlyIssue(configurationScopeId, commentedIssue); } } else { throw issueNotFoundException(issueId.toString()); } } private static ResponseErrorException issueNotFoundException(String issueId) { var error = new ResponseError(SonarLintRpcErrorCode.ISSUE_NOT_FOUND, "Issue key " + issueId + " was not found", issueId); throw new ResponseErrorException(error); } private void addCommentOnServerIssue(String configurationScopeId, String issueKey, String comment, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()) .withClientApi(serverApi -> serverApi.issue().addComment(issueKey, comment, cancelMonitor)); } private boolean reopenServerIssue(ServerApi connection, Binding binding, String issueId, ProjectServerIssueStore projectServerIssueStore, boolean isTaintIssue, SonarLintCancelMonitor cancelMonitor) { connection.issue().changeStatus(issueId, Transition.REOPEN, cancelMonitor); var serverIssue = projectServerIssueStore.updateIssueResolutionStatus(issueId, isTaintIssue, false); serverIssue.ifPresent(issue -> eventPublisher.publishEvent(new ServerIssueStatusChangedEvent(binding.connectionId(), binding.sonarProjectKey(), issue))); return true; } private boolean reopenLocalIssue(String issueId, String configurationScopeId, SonarLintCancelMonitor cancelMonitor) { var issueUuidOptional = asUUID(issueId); if (issueUuidOptional.isEmpty()) { return false; } var issueUuid = issueUuidOptional.get(); removeIssueOnServer(configurationScopeId, issueUuid, cancelMonitor); return localOnlyIssuesRepository.removeIssue(issueUuid); } public EffectiveIssueDetailsDto getEffectiveIssueDetails(String configurationScopeId, UUID findingId, SonarLintCancelMonitor cancelMonitor) throws IssueNotFoundException, RuleNotFoundException { var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); String connectionId = null; if (effectiveBinding.isPresent()) { connectionId = effectiveBinding.get().connectionId(); } var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); var aiCodeFixFeature = effectiveBinding.flatMap(aiCodeFixService::getFeature); var maybeIssue = findingReportingService.findReportedIssue(findingId, newCodeDefinition, isMQRMode, aiCodeFixFeature); var maybeHotspot = findingReportingService.findReportedHotspot(findingId, newCodeDefinition, isMQRMode); var maybeTaint = taintVulnerabilityTrackingService.getTaintVulnerability(configurationScopeId, findingId, cancelMonitor); if (maybeIssue != null) { return getFindingDetails(maybeIssue, configurationScopeId, cancelMonitor); } else if (maybeHotspot != null) { return getFindingDetails(maybeHotspot, configurationScopeId, cancelMonitor); } else if (maybeTaint.isPresent()) { return getTaintDetails(maybeTaint.get(), configurationScopeId, cancelMonitor); } throw new IssueNotFoundException("Failed to retrieve finding details. Finding with key '" + findingId + "' not found.", findingId); } private EffectiveIssueDetailsDto getFindingDetails(RaisedFindingDto finding, String configurationScopeId, SonarLintCancelMonitor cancelMonitor) throws RuleNotFoundException { var ruleKey = finding.getRuleKey(); var ruleDetails = activeRulesService.getActiveRuleDetails(configurationScopeId, ruleKey, cancelMonitor); var ruleDetailsEnrichedWithActualIssueSeverity = RuleDetails.merging(ruleDetails, finding); var effectiveRuleDetails = RuleDetailsAdapter.transform(ruleDetailsEnrichedWithActualIssueSeverity, finding.getRuleDescriptionContextKey()); return new EffectiveIssueDetailsDto(ruleKey, effectiveRuleDetails.getName(), effectiveRuleDetails.getLanguage(), // users cannot customize vulnerability probability effectiveRuleDetails.getVulnerabilityProbability(), effectiveRuleDetails.getDescription(), effectiveRuleDetails.getParams(), finding.getSeverityMode(), finding.getRuleDescriptionContextKey()); } private EffectiveIssueDetailsDto getTaintDetails(TaintVulnerabilityDto finding, String configurationScopeId, SonarLintCancelMonitor cancelMonitor) throws RuleNotFoundException { var ruleKey = finding.getRuleKey(); var ruleDetails = activeRulesService.getActiveRuleDetails(configurationScopeId, ruleKey, cancelMonitor); var ruleDetailsEnrichedWithActualIssueSeverity = RuleDetails.merging(ruleDetails, finding); var effectiveRuleDetails = RuleDetailsAdapter.transform(ruleDetailsEnrichedWithActualIssueSeverity, finding.getRuleDescriptionContextKey()); return new EffectiveIssueDetailsDto(ruleKey, effectiveRuleDetails.getName(), effectiveRuleDetails.getLanguage(), effectiveRuleDetails.getVulnerabilityProbability(), effectiveRuleDetails.getDescription(), effectiveRuleDetails.getParams(), finding.getSeverityMode(), finding.getRuleDescriptionContextKey()); } @EventListener public void onServerEventReceived(SonarServerEventReceivedEvent eventReceived) { var connectionId = eventReceived.getConnectionId(); var serverEvent = eventReceived.getEvent(); if (serverEvent instanceof IssueChangedEvent issueChangedEvent) { handleEvent(connectionId, issueChangedEvent); } } private void handleEvent(String connectionId, IssueChangedEvent event) { updateProjectIssueStorage(connectionId, event); republishPreviouslyRaisedIssues(connectionId, event); } private void republishPreviouslyRaisedIssues(String connectionId, IssueChangedEvent event) { var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var boundScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, event.getProjectKey()); boundScopes.forEach(scope -> { var scopeId = scope.getConfigScopeId(); findingReportingService.updateAndReportIssues(scopeId, previouslyRaisedIssue -> raisedIssueUpdater(previouslyRaisedIssue, isMQRMode, event)); }); } public static RaisedIssueDto raisedIssueUpdater(RaisedIssueDto previouslyRaisedIssue, boolean isMQRMode, IssueChangedEvent event) { var updatedIssue = previouslyRaisedIssue; var resolved = event.getResolved(); var userSeverity = event.getUserSeverity(); var userType = event.getUserType(); var impactedIssueKeys = event.getImpactedIssues().stream().map(IssueChangedEvent.Issue::getIssueKey).collect(Collectors.toSet()); if (resolved != null) { UnaryOperator issueUpdater = it -> it.builder().withResolution(resolved).buildIssue(); updatedIssue = updateIssue(updatedIssue, impactedIssueKeys, issueUpdater); } if (updatedIssue.getSeverityMode().isLeft()) { // if the event does not match the local severity mode, we skip updating as we would only have partial information // the data will be updated at the next sync var standardModeDetails = updatedIssue.getSeverityMode().getLeft(); if (userSeverity != null) { UnaryOperator issueUpdater = it -> it.builder().withStandardModeDetails(IssueSeverity.valueOf(userSeverity.name()), standardModeDetails.getType()) .buildIssue(); updatedIssue = updateIssue(updatedIssue, impactedIssueKeys, issueUpdater); } if (userType != null) { UnaryOperator issueUpdater = it -> it.builder().withStandardModeDetails(standardModeDetails.getSeverity(), RuleType.valueOf(userType.name())) .buildIssue(); updatedIssue = updateIssue(updatedIssue, impactedIssueKeys, issueUpdater); } } for (var issue : event.getImpactedIssues()) { if (!issue.getImpacts().isEmpty() && isMQRMode && updatedIssue.getSeverityMode().isRight()) { var mqrModeDetails = updatedIssue.getSeverityMode().getRight(); var impacts = issue.getImpacts().entrySet().stream() .map(impact -> new ImpactDto( SoftwareQuality.valueOf(impact.getKey().name()), org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity.valueOf(impact.getValue().name()))) .toList(); UnaryOperator issueUpdater = it -> it.builder() .withMQRModeDetails(mqrModeDetails.getCleanCodeAttribute(), mergeImpacts(it.getSeverityMode().getRight().getImpacts(), impacts)).buildIssue(); updatedIssue = updateIssue(updatedIssue, impactedIssueKeys, issueUpdater); } } return updatedIssue; } private static List mergeImpacts(List currentImpacts, List overriddenImpacts) { var mergedImpacts = new ArrayList<>(currentImpacts); for (var impact : overriddenImpacts) { mergedImpacts.removeIf(i -> i.getSoftwareQuality().equals(impact.getSoftwareQuality())); mergedImpacts.add(new ImpactDto(impact.getSoftwareQuality(), impact.getImpactSeverity())); } return mergedImpacts; } private static RaisedIssueDto updateIssue(RaisedIssueDto issue, Set impactedIssueKeys, UnaryOperator issueUpdater) { var serverKey = issue.getServerKey(); if (serverKey != null && impactedIssueKeys.contains(serverKey)) { return issueUpdater.apply(issue); } return issue; } private void updateProjectIssueStorage(String connectionId, IssueChangedEvent event) { var findingsStorage = storageService.connection(connectionId).project(event.getProjectKey()).findings(); event.getImpactedIssues().forEach(issue -> findingsStorage.updateIssue(issue.getIssueKey(), storedIssue -> { var userSeverity = event.getUserSeverity(); if (userSeverity != null) { storedIssue.setUserSeverity(userSeverity); } var userType = event.getUserType(); if (userType != null) { storedIssue.setType(userType); } var resolved = event.getResolved(); if (resolved != null) { storedIssue.setResolved(resolved); } var impacts = issue.getImpacts(); if (!impacts.isEmpty()) { storedIssue.setImpacts(mergeImpacts(storedIssue.getImpacts(), impacts)); } })); } private static Map mergeImpacts( Map defaultImpacts, Map overriddenImpacts) { var mergedImpacts = new EnumMap(org.sonarsource.sonarlint.core.commons.SoftwareQuality.class); if (!defaultImpacts.isEmpty()) { mergedImpacts = new EnumMap<>(defaultImpacts); } for (var entry : overriddenImpacts.entrySet()) { var quality = org.sonarsource.sonarlint.core.commons.SoftwareQuality.valueOf(entry.getKey().name()); var severity = ImpactSeverity.mapSeverity(entry.getValue().name()); mergedImpacts.put(quality, severity); } return Collections.unmodifiableMap(mergedImpacts); } private static Optional asUUID(String key) { try { return Optional.of(UUID.fromString(key)); } catch (Exception e) { return Optional.empty(); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/issue/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.issue; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/IdeLabsHttpClient.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.labs; import com.google.gson.Gson; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.springframework.beans.factory.annotation.Qualifier; public class IdeLabsHttpClient { private final HttpClient httpClient; private final String labsSubscriptionEndpoint; private final Gson gson = new Gson(); public IdeLabsHttpClient(HttpClientProvider httpClientProvider, @Qualifier("labsSubscriptionEndpoint") String labsSubscriptionEndpoint) { this.httpClient = httpClientProvider.getHttpClientWithoutAuth(); this.labsSubscriptionEndpoint = labsSubscriptionEndpoint; } public HttpClient.Response join(String email, String ideName) { var requestBody = gson.toJson(new IdeLabsSubscriptionRequestPayload(email, ideName)); return httpClient.post(labsSubscriptionEndpoint, "application/json", requestBody); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/IdeLabsService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.labs; import com.google.gson.Gson; import org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.JoinIdeLabsProgramResponse; public class IdeLabsService { private final IdeLabsHttpClient labsHttpClient; private final Gson gson = new Gson(); public IdeLabsService(IdeLabsHttpClient labsHttpClient) { this.labsHttpClient = labsHttpClient; } public JoinIdeLabsProgramResponse joinIdeLabsProgram(String email, String ideName) { try (var response = labsHttpClient.join(email, ideName)) { if (!response.isSuccessful()) { return new JoinIdeLabsProgramResponse(false, "An unexpected error occurred. Server responded with status code: " + response.code()); } var responseBody = gson.fromJson(response.bodyAsString(), IdeLabsSubscriptionResponseBody.class); if (!responseBody.validEmail()) { return new JoinIdeLabsProgramResponse(false, "The provided email address is not valid. Please enter a valid email address."); } return new JoinIdeLabsProgramResponse(true, null); } catch (Exception e) { return new JoinIdeLabsProgramResponse(false, "An unexpected error occurred: " + e.getMessage()); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/IdeLabsSpringConfig.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.labs; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import({IdeLabsHttpClient.class, IdeLabsService.class}) public class IdeLabsSpringConfig { public static final String PROPERTY_IDE_LABS_SUBSCRIPTION_URL = "sonarlint.internal.labs.subscription.url"; public static final String IDE_LABS_SUBSCRIPTION_URL = "https://discover.sonarsource.com/sq-ide-labs.json"; @Bean(name = "labsSubscriptionEndpoint") String provideLabsSubscriptionEndpoint() { return System.getProperty(PROPERTY_IDE_LABS_SUBSCRIPTION_URL, IDE_LABS_SUBSCRIPTION_URL); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/IdeLabsSubscriptionRequestPayload.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.labs; public record IdeLabsSubscriptionRequestPayload(String email, String source) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/IdeLabsSubscriptionResponseBody.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.labs; import com.google.gson.annotations.SerializedName; public record IdeLabsSubscriptionResponseBody(@SerializedName("valid_email") boolean validEmail) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/labs/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.labs; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/languages/LanguageSupportRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.languages; import java.util.Collection; import java.util.EnumSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; public class LanguageSupportRepository { private static final EnumSet LANGUAGES_RAISING_TAINT_VULNERABILITIES = EnumSet.of(SonarLanguage.CS, SonarLanguage.JAVA, SonarLanguage.JS, SonarLanguage.TS, SonarLanguage.PHP, SonarLanguage.PYTHON); private final EnumSet enabledLanguagesInStandaloneMode; private final EnumSet extraEnabledLanguagesInConnectedMode; private final EnumSet enabledLanguagesInConnectedMode; public LanguageSupportRepository(InitializeParams params) { this.enabledLanguagesInStandaloneMode = toEnumSet(adaptLanguage(params.getEnabledLanguagesInStandaloneMode()), SonarLanguage.class); this.enabledLanguagesInConnectedMode = EnumSet.copyOf(this.enabledLanguagesInStandaloneMode); this.extraEnabledLanguagesInConnectedMode = toEnumSet(adaptLanguage(params.getExtraEnabledLanguagesInConnectedMode()), SonarLanguage.class); this.enabledLanguagesInConnectedMode.addAll(extraEnabledLanguagesInConnectedMode); } @NotNull private static List adaptLanguage(Set languagesDto) { return languagesDto.stream().map(e -> SonarLanguage.valueOf(e.name())).toList(); } private static > EnumSet toEnumSet(Collection collection, Class clazz) { return collection.isEmpty() ? EnumSet.noneOf(clazz) : EnumSet.copyOf(collection); } public Set getEnabledLanguagesInStandaloneMode() { return enabledLanguagesInStandaloneMode; } public Set getEnabledLanguagesInConnectedMode() { return enabledLanguagesInConnectedMode; } public boolean areTaintVulnerabilitiesSupported() { var intersection = EnumSet.copyOf(LANGUAGES_RAISING_TAINT_VULNERABILITIES); intersection.retainAll(enabledLanguagesInConnectedMode); return !intersection.isEmpty(); } public boolean isEnabledOnlyInConnectedMode(SonarLanguage language) { return extraEnabledLanguagesInConnectedMode.contains(language); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/languages/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.languages; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/local/only/IssueStatusBinding.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.local.only; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.IssueStatus; public class IssueStatusBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return IssueStatus.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final var cPair = (IssueStatus) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80_000_000); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/local/only/XodusLocalOnlyIssueStorageService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.local.only; import jakarta.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.UserPaths; import static org.sonarsource.sonarlint.core.commons.storage.XodusPurgeUtils.deleteInFolderWithPattern; import static org.sonarsource.sonarlint.core.local.only.XodusLocalOnlyIssueStore.LOCAL_ONLY_ISSUE; public class XodusLocalOnlyIssueStorageService { private final Path projectsStorageBaseDir; private final Path workDir; private XodusLocalOnlyIssueStore localOnlyIssueStore; public XodusLocalOnlyIssueStorageService(UserPaths userPaths) { this.projectsStorageBaseDir = userPaths.getStorageRoot(); this.workDir = userPaths.getWorkDir(); } public boolean exists() { return Files.exists(projectsStorageBaseDir.resolve(XodusLocalOnlyIssueStore.BACKUP_TAR_GZ)); } public XodusLocalOnlyIssueStore get() { if (localOnlyIssueStore == null) { try { localOnlyIssueStore = new XodusLocalOnlyIssueStore(projectsStorageBaseDir, workDir); return localOnlyIssueStore; } catch (IOException e) { throw new IllegalStateException("Unable to create local-only issue database", e); } } return localOnlyIssueStore; } @PreDestroy public void close() { if (localOnlyIssueStore != null) { localOnlyIssueStore.close(); } } public void delete() { if (localOnlyIssueStore != null) { localOnlyIssueStore.close(); localOnlyIssueStore = null; } FileUtils.deleteQuietly(projectsStorageBaseDir.resolve(XodusLocalOnlyIssueStore.BACKUP_TAR_GZ).toFile()); deleteInFolderWithPattern(workDir, LOCAL_ONLY_ISSUE + "*"); deleteInFolderWithPattern(projectsStorageBaseDir, "local_only_issue_backup*"); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/local/only/XodusLocalOnlyIssueStore.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.local.only; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.StreamSupport; import jetbrains.exodus.entitystore.Entity; import jetbrains.exodus.entitystore.PersistentEntityStore; import jetbrains.exodus.entitystore.PersistentEntityStores; import jetbrains.exodus.env.EnvironmentConfig; import jetbrains.exodus.env.Environments; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssueResolution; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.storage.InstantBinding; import org.sonarsource.sonarlint.core.serverconnection.storage.TarGzUtils; import org.sonarsource.sonarlint.core.serverconnection.storage.UuidBinding; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.flatMapping; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; public class XodusLocalOnlyIssueStore { static final String LOCAL_ONLY_ISSUE = "xodus-local-only-issue-store"; private static final String CONFIGURATION_SCOPE_ID_ENTITY_TYPE = "Scope"; private static final String CONFIGURATION_SCOPE_ID_TO_FILES_LINK_NAME = "files"; private static final String PATH_PROPERTY_NAME = "path"; private static final String NAME_PROPERTY_NAME = "name"; private static final String FILE_TO_ISSUES_LINK_NAME = "issues"; private static final String UUID_PROPERTY_NAME = "uuid"; private static final String ISSUE_TO_FILE_LINK_NAME = "file"; private static final String COMMENT_PROPERTY_NAME = "comment"; private static final String RESOLVED_STATUS_PROPERTY_NAME = "resolvedStatus"; private static final String RESOLUTION_DATE_PROPERTY_NAME = "resolvedDate"; private static final String RULE_KEY_PROPERTY_NAME = "ruleKey"; private static final String RANGE_HASH_PROPERTY_NAME = "rangeHash"; private static final String LINE_HASH_PROPERTY_NAME = "lineHash"; private static final String START_LINE_PROPERTY_NAME = "startLine"; private static final String START_LINE_OFFSET_PROPERTY_NAME = "startLineOffset"; private static final String END_LINE_PROPERTY_NAME = "endLine"; private static final String END_LINE_OFFSET_PROPERTY_NAME = "endLineOffset"; private static final String MESSAGE_BLOB_NAME = "message"; static final String BACKUP_TAR_GZ = "local_only_issue_backup.tar.gz"; private final PersistentEntityStore entityStore; private final Path xodusDbDir; private static final SonarLintLogger LOG = SonarLintLogger.get(); public XodusLocalOnlyIssueStore(Path backupDir, Path workDir) throws IOException { xodusDbDir = Files.createTempDirectory(workDir, LOCAL_ONLY_ISSUE); var backupFile = backupDir.resolve(BACKUP_TAR_GZ); if (Files.isRegularFile(backupFile)) { LOG.debug("Restoring previous local-only issue database from {}", backupFile); try { TarGzUtils.extractTarGz(backupFile, xodusDbDir); } catch (Exception e) { LOG.error("Unable to restore local-only issue backup {}", backupFile); } } LOG.debug("Starting local-only issue database from {}", xodusDbDir); this.entityStore = buildEntityStore(); entityStore.executeInTransaction(txn -> { entityStore.registerCustomPropertyType(txn, Instant.class, new InstantBinding()); entityStore.registerCustomPropertyType(txn, UUID.class, new UuidBinding()); entityStore.registerCustomPropertyType(txn, IssueStatus.class, new IssueStatusBinding()); }); } public Map> loadAll() { return entityStore.computeInReadonlyTransaction(txn -> StreamSupport.stream(txn.getAll(CONFIGURATION_SCOPE_ID_ENTITY_TYPE).spliterator(), false) .collect(groupingBy( e -> (String) requireNonNull(e.getProperty(NAME_PROPERTY_NAME)), flatMapping(e -> StreamSupport.stream(e.getLinks(CONFIGURATION_SCOPE_ID_TO_FILES_LINK_NAME).spliterator(), false) .flatMap(file -> StreamSupport.stream(file.getLinks(XodusLocalOnlyIssueStore.FILE_TO_ISSUES_LINK_NAME).spliterator(), false) .map(XodusLocalOnlyIssueStore::adapt)), toList())))); } private static LocalOnlyIssue adapt(Entity storedIssue) { var filePath = (String) requireNonNull(storedIssue.getLink(ISSUE_TO_FILE_LINK_NAME).getProperty(PATH_PROPERTY_NAME)); var uuid = (UUID) requireNonNull(storedIssue.getProperty(UUID_PROPERTY_NAME)); var status = (IssueStatus) requireNonNull(storedIssue.getProperty(RESOLVED_STATUS_PROPERTY_NAME)); var resolvedDate = (Instant) requireNonNull(storedIssue.getProperty(RESOLUTION_DATE_PROPERTY_NAME)); var ruleKey = (String) requireNonNull(storedIssue.getProperty(RULE_KEY_PROPERTY_NAME)); var msg = requireNonNull(storedIssue.getBlobString(MESSAGE_BLOB_NAME)); var comment = storedIssue.getBlobString(COMMENT_PROPERTY_NAME); var startLine = (Integer) storedIssue.getProperty(START_LINE_PROPERTY_NAME); TextRangeWithHash textRange = null; LineWithHash lineWithHash = null; if (startLine != null) { var rangeHash = (String) storedIssue.getProperty(RANGE_HASH_PROPERTY_NAME); if (rangeHash != null) { var startLineOffset = (Integer) storedIssue.getProperty(START_LINE_OFFSET_PROPERTY_NAME); var endLine = (Integer) storedIssue.getProperty(END_LINE_PROPERTY_NAME); var endLineOffset = (Integer) storedIssue.getProperty(END_LINE_OFFSET_PROPERTY_NAME); textRange = new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, rangeHash); } var lineHash = (String) storedIssue.getProperty(LINE_HASH_PROPERTY_NAME); if (lineHash != null) { lineWithHash = new LineWithHash(startLine, lineHash); } } return new LocalOnlyIssue( uuid, Path.of(filePath), textRange, lineWithHash, ruleKey, msg, new LocalOnlyIssueResolution(status, resolvedDate, comment)); } private PersistentEntityStore buildEntityStore() { var environment = Environments.newInstance(xodusDbDir.toAbsolutePath().toFile(), new EnvironmentConfig() .setLogAllowRemote(true) .setLogAllowRemovable(true) .setLogAllowRamDisk(true)); var entityStoreImpl = PersistentEntityStores.newInstance(environment); entityStoreImpl.setCloseEnvironment(true); return entityStoreImpl; } public void close() { entityStore.close(); FileUtils.deleteQuietly(xodusDbDir.toFile()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/local/only/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.local.only; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/log/LogService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.log; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.rpc.protocol.backend.log.LogLevel; public class LogService { public void setLogLevel(LogLevel newLevel) { SonarLintLogger.get().setLevel(convert(newLevel)); } public static LogOutput.Level convert(LogLevel level) { return switch (level) { case OFF -> LogOutput.Level.OFF; case ERROR -> LogOutput.Level.ERROR; case INFO -> LogOutput.Level.INFO; case WARN -> LogOutput.Level.WARN; case DEBUG -> LogOutput.Level.DEBUG; case TRACE -> LogOutput.Level.TRACE; }; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/log/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.log; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/mode/SeverityModeService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.mode; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.storage.StorageService; public class SeverityModeService { private final StorageService storageService; private final ConnectionConfigurationRepository connectionConfigurationRepository; public SeverityModeService(StorageService storageService, ConnectionConfigurationRepository connectionConfigurationRepository) { this.storageService = storageService; this.connectionConfigurationRepository = connectionConfigurationRepository; } public boolean isMQRModeForConnection(@Nullable String connectionId) { if (connectionId == null) { return true; } var connection = connectionConfigurationRepository.getConnectionById(connectionId); if (connection == null) { throw new IllegalArgumentException("Connection with id '" + connectionId + "' not found"); } if (connection.getKind() == ConnectionKind.SONARCLOUD) { return true; } return storageService.connection(connectionId).serverInfo().read() .map(StoredServerInfo::shouldConsiderMultiQualityModeEnabled) // if no storage, use MQR .orElse(true); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/mode/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.mode; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/monitoring/MonitoringInitializationParams.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.monitoring; public record MonitoringInitializationParams( boolean monitoringEnabled, boolean isTelemetryEnabled, String productKey, String sonarQubeForIdeVersion, String ideVersion ) {} ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/monitoring/MonitoringService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.monitoring; import io.sentry.Hint; import io.sentry.Sentry; import io.sentry.SentryBaseEvent; import io.sentry.SentryOptions; import io.sentry.protocol.User; import jakarta.inject.Inject; import org.apache.commons.lang3.SystemUtils; import org.sonarsource.sonarlint.core.commons.SonarLintCoreVersion; import org.sonarsource.sonarlint.core.commons.dogfood.DogfoodEnvironmentDetectionService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import org.sonarsource.sonarlint.core.event.TelemetryUpdatedEvent; import org.springframework.context.event.EventListener; public class MonitoringService { public static final String DSN_PROPERTY = "sonarlint.internal.monitoring.dsn"; private static final String DSN_DEFAULT = "https://ad1c1fe3cb2b12fc2d191ecd25f89866@o1316750.ingest.us.sentry.io/4508201175089152"; public static final String TRACES_SAMPLE_RATE_PROPERTY = "sonarlint.internal.monitoring.tracesSampleRate"; private static final double TRACES_SAMPLE_RATE_DEFAULT = 0D; private static final double TRACES_SAMPLE_RATE_DOGFOOD_DEFAULT = 0.01D; private static final String ENVIRONMENT_PRODUCTION = "production"; private static final String ENVIRONMENT_DOGFOOD = "dogfood"; private static final SonarLintLogger LOG = SonarLintLogger.get(); private final MonitoringInitializationParams initializeParams; private final DogfoodEnvironmentDetectionService dogfoodEnvDetectionService; private final MonitoringUserIdStore userIdStore; private boolean active; @Inject public MonitoringService(MonitoringInitializationParams initializeParams, DogfoodEnvironmentDetectionService dogfoodEnvDetectionService, MonitoringUserIdStore userIdStore) { this.initializeParams = initializeParams; this.dogfoodEnvDetectionService = dogfoodEnvDetectionService; this.userIdStore = userIdStore; this.startIfNeeded(); } public void startIfNeeded() { if (!initializeParams.monitoringEnabled()) { LOG.info("Monitoring is disabled by feature flag."); return; } if (shouldInitializeSentry()) { LOG.info("Initializing Sentry"); start(); } } private boolean shouldInitializeSentry() { return dogfoodEnvDetectionService.isDogfoodEnvironment() || initializeParams.isTelemetryEnabled(); } private void start() { Sentry.init(this::configure); userIdStore.getOrCreate().ifPresent(userId -> { var user = new User(); user.setId(userId.toString()); Sentry.setUser(user); }); active = true; } public boolean isActive() { return active; } private void configure(SentryOptions sentryOptions) { sentryOptions.setDsn(getDsn()); sentryOptions.setRelease(SonarLintCoreVersion.getLibraryVersion()); sentryOptions.setEnvironment(getEnvironment()); sentryOptions.setTag("productKey", initializeParams.productKey()); sentryOptions.setTag("sonarQubeForIDEVersion", initializeParams.sonarQubeForIdeVersion()); sentryOptions.setTag("ideVersion", initializeParams.ideVersion()); sentryOptions.setTag("platform", SystemUtils.OS_NAME); sentryOptions.setTag("architecture", SystemUtils.OS_ARCH); sentryOptions.addInAppInclude("org.sonarsource.sonarlint"); sentryOptions.setTracesSampleRate(getTracesSampleRate()); addCaptureIgnoreRule(sentryOptions, "(?s)com\\.sonar\\.sslr\\.api\\.RecognitionException.*"); addCaptureIgnoreRule(sentryOptions, "(?s)com\\.sonar\\.sslr\\.impl\\.LexerException.*"); sentryOptions.setBeforeSend(MonitoringService::beforeSend); sentryOptions.setBeforeSendTransaction(MonitoringService::beforeSend); } private String getEnvironment() { if (dogfoodEnvDetectionService.isDogfoodEnvironment()) { return ENVIRONMENT_DOGFOOD; } return ENVIRONMENT_PRODUCTION; } private static T beforeSend(T event, Hint hint) { event.setServerName(null); return event; } private static String getDsn() { return System.getProperty(DSN_PROPERTY, DSN_DEFAULT); } private double getTracesSampleRate() { try { var sampleRateFromSystemProperty = System.getProperty(TRACES_SAMPLE_RATE_PROPERTY); var parsedSampleRate = Double.parseDouble(sampleRateFromSystemProperty); LOG.debug("Overriding trace sample rate with value from system property: {}", parsedSampleRate); return parsedSampleRate; } catch (RuntimeException e) { var sampleRate = TRACES_SAMPLE_RATE_DEFAULT; if (dogfoodEnvDetectionService.isDogfoodEnvironment()) { sampleRate = TRACES_SAMPLE_RATE_DOGFOOD_DEFAULT; } LOG.debug("Using default trace sample rate: {}", sampleRate); return sampleRate; } } /** * To ignore exceptions, it's better to use {@link SentryOptions#addIgnoredExceptionForType}, but it accepts Class type * and this is the workaround for the case when Exception class is not in the classpath * * @param regex this should be the regex satisfying java.util.regex.Pattern spec */ private static void addCaptureIgnoreRule(SentryOptions sentryOptions, String regex) { sentryOptions.addIgnoredError(regex); } public Trace newTrace(String name, String operation) { return Trace.begin(name, operation); } @EventListener public void onTelemetryUpdated(TelemetryUpdatedEvent event) { if (!event.isTelemetryEnabled()) { Sentry.close(); active = false; } else if (!active && initializeParams.monitoringEnabled() && shouldInitializeSentry()) { LOG.info("Initializing Sentry after telemetry was enabled"); start(); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/monitoring/MonitoringUserIdStore.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.monitoring; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Base64; import java.util.Optional; import java.util.UUID; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class MonitoringUserIdStore { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String USER_ID_FILE_NAME = "id"; private final Path path; @Nullable private volatile UUID cachedUserId; public MonitoringUserIdStore(UserPaths userPaths) { this.path = userPaths.getUserHome().resolve(USER_ID_FILE_NAME); } public synchronized Optional getOrCreate() { var cached = cachedUserId; if (cached != null) { return Optional.of(cached); } try { Files.createDirectories(path.getParent()); try (var fileChannel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.SYNC); var ignored = fileChannel.lock()) { var userId = readOrCreateUserId(fileChannel); cachedUserId = userId; return Optional.of(userId); } } catch (Exception e) { LOG.debug("Failed to read or create user ID", e); return Optional.empty(); } } private static UUID readOrCreateUserId(FileChannel fileChannel) throws IOException { var existingId = readUserId(fileChannel); if (existingId != null) { return existingId; } var newId = UUID.randomUUID(); writeUserId(fileChannel, newId); return newId; } @Nullable private static UUID readUserId(FileChannel fileChannel) throws IOException { if (fileChannel.size() == 0) { return null; } var buffer = ByteBuffer.allocate((int) fileChannel.size()); fileChannel.read(buffer); try { var decoded = Base64.getDecoder().decode(buffer.array()); var content = new String(decoded, StandardCharsets.UTF_8).trim(); if (content.isEmpty()) { return null; } return UUID.fromString(content); } catch (IllegalArgumentException e) { LOG.debug("Invalid encoded UUID in " + USER_ID_FILE_NAME, e); return null; } } private static void writeUserId(FileChannel fileChannel, UUID userId) throws IOException { fileChannel.truncate(0); fileChannel.position(0); var encoded = Base64.getEncoder().encode(userId.toString().getBytes(StandardCharsets.UTF_8)); fileChannel.write(ByteBuffer.wrap(encoded)); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/monitoring/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.monitoring; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/newcode/NewCodeService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.newcode; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.GetNewCodeDefinitionResponse; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; public class NewCodeService { private static final NewCodeDefinition STANDALONE_NEW_CODE_DEFINITION = NewCodeDefinition.withExactNumberOfDays(30); private final ConfigurationRepository configurationRepository; private final StorageService storageService; private final TelemetryService telemetryService; public NewCodeService(ConfigurationRepository configurationRepository, StorageService storageService, TelemetryService telemetryService) { this.configurationRepository = configurationRepository; this.storageService = storageService; this.telemetryService = telemetryService; } public GetNewCodeDefinitionResponse getNewCodeDefinition(String configScopeId) { return getFullNewCodeDefinition(configScopeId) .map(newCodeDefinition -> new GetNewCodeDefinitionResponse(newCodeDefinition.toString(), newCodeDefinition.isSupported())) .orElse(new GetNewCodeDefinitionResponse("No new code definition found", false)); } public Optional getFullNewCodeDefinition(String configScopeId) { var effectiveBinding = configurationRepository.getEffectiveBinding(configScopeId); if (effectiveBinding.isEmpty()) { return Optional.of(STANDALONE_NEW_CODE_DEFINITION); } var binding = effectiveBinding.get(); var sonarProjectStorage = storageService.binding(binding); return sonarProjectStorage.newCodeDefinition().read(); } public void didToggleFocus() { telemetryService.newCodeFocusChanged(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/newcode/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.newcode; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/nodejs/InstalledNodeJs.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.nodejs; import java.nio.file.Path; import org.sonarsource.sonarlint.core.commons.Version; public class InstalledNodeJs { private final Path path; private final Version version; public InstalledNodeJs(Path path, Version version) { this.path = path; this.version = version; } public Path getPath() { return path; } public Version getVersion() { return version; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/nodejs/NodeJsHelper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.nodejs; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandException; import org.sonar.api.utils.command.CommandExecutor; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class NodeJsHelper { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Pattern NODEJS_VERSION_PATTERN = Pattern.compile("v?(\\d+\\.\\d+\\.\\d+(?:-\\S+)?)"); private final System2 system2; private final Path pathHelperLocationOnMac; private final CommandExecutor commandExecutor; public NodeJsHelper() { this(System2.INSTANCE, Paths.get("/usr/libexec/path_helper"), CommandExecutor.create()); } // For testing NodeJsHelper(System2 system2, Path pathHelperLocationOnMac, CommandExecutor commandExecutor) { this.system2 = system2; this.pathHelperLocationOnMac = pathHelperLocationOnMac; this.commandExecutor = commandExecutor; } @CheckForNull public InstalledNodeJs autoDetect() { return detect(null); } @CheckForNull public InstalledNodeJs detect(@Nullable Path configuredNodejsPath) { var detectedNodePath = locateNode(configuredNodejsPath); if (detectedNodePath != null) { var nodeJsVersion = readNodeVersion(detectedNodePath); if (nodeJsVersion == null) { LOG.warn("Unable to query node version"); } else { return new InstalledNodeJs(detectedNodePath, nodeJsVersion); } } return null; } @CheckForNull private Version readNodeVersion(Path detectedNodePath) { LOG.debug("Checking node version..."); String nodeVersionStr; var forcedNodeVersion = System.getProperty("sonarlint.internal.nodejs.forcedVersion"); if (forcedNodeVersion != null) { nodeVersionStr = forcedNodeVersion; } else { var command = Command.create(detectedNodePath.toString()).addArgument("-v"); nodeVersionStr = runSimpleCommand(command); } Version nodeJsVersion = null; if (nodeVersionStr != null) { var matcher = NODEJS_VERSION_PATTERN.matcher(nodeVersionStr); if (matcher.matches()) { var version = matcher.group(1); nodeJsVersion = Version.create(version); LOG.debug("Detected node version: {}", nodeJsVersion); } else { LOG.debug("Unable to parse node version: {}", nodeVersionStr); } } return nodeJsVersion; } @CheckForNull private Path locateNode(@Nullable Path configuredNodejsPath) { if (configuredNodejsPath != null) { LOG.debug("Node.js path provided by configuration: {}", configuredNodejsPath); return configuredNodejsPath; } LOG.debug("Looking for node in the PATH"); var forcedPath = System.getProperty("sonarlint.internal.nodejs.forcedPath"); String result; if (forcedPath != null) { result = forcedPath; } else if (system2.isOsWindows()) { result = runSimpleCommand(Command.create("C:\\Windows\\System32\\where.exe").addArgument("$PATH:node.exe")); } else { // INFO: Based on the Linux / macOS shell we require the full path as "which" is a built-in on some shells! var which = Command.create("/usr/bin/which").addArgument("node"); computePathEnvForMacOs(which); result = runSimpleCommand(which); } if (result != null) { LOG.debug("Found node at {}", result); return Paths.get(result); } else { LOG.debug("Unable to locate node"); return null; } } private void computePathEnvForMacOs(Command which) { if (system2.isOsMac() && Files.exists(pathHelperLocationOnMac)) { var command = Command.create(pathHelperLocationOnMac.toString()).addArgument("-s"); var pathHelperOutput = runSimpleCommand(command); if (pathHelperOutput != null) { var regex = Pattern.compile("^\\s*PATH=\"([^\"]+)\"; export PATH;?\\s*$"); var matchResult = regex.matcher(pathHelperOutput); if (matchResult.matches()) { which.setEnvironmentVariable("PATH", matchResult.group(1)); } } } } /** * Run a simple command that should return a single line on stdout */ @CheckForNull private String runSimpleCommand(Command command) { List stdOut = new ArrayList<>(); List stdErr = new ArrayList<>(); LOG.debug("Execute command '{}'...", command); int exitCode; try { exitCode = commandExecutor.execute(command, stdOut::add, stdErr::add, 10_000); } catch (CommandException e) { LOG.debug("Unable to execute the command", e); return null; } var msg = new StringBuilder(String.format("Command '%s' exited with %s", command, exitCode)); if (!stdOut.isEmpty()) { msg.append("\nstdout: ").append(String.join("\n", stdOut)); } if (!stdErr.isEmpty()) { msg.append("\nstderr: ").append(String.join("\n", stdErr)); } LOG.debug("{}", msg); if (exitCode != 0 || stdOut.isEmpty()) { return null; } return stdOut.get(0); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/nodejs/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.nodejs; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/ArtifactSource.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; public enum ArtifactSource { EMBEDDED, ON_DEMAND, SONARQUBE_SERVER, SONARQUBE_CLOUD } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/DotnetSupport.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; public class DotnetSupport { @Nullable private final Path actualCsharpAnalyzerPath; private final boolean supportsCsharp; private final boolean supportsVbNet; private final boolean shouldUseCsharpEnterprise; private final boolean shouldUseVbNetEnterprise; DotnetSupport(InitializeParams initializeParams, @Nullable Path actualCsharpAnalyzerPath, boolean shouldUseCsharpEnterprise, boolean shouldUseVbNetEnterprise) { supportsCsharp = initializeParams.getEnabledLanguagesInStandaloneMode().contains(Language.CS); supportsVbNet = initializeParams.getEnabledLanguagesInStandaloneMode().contains(Language.VBNET); this.actualCsharpAnalyzerPath = actualCsharpAnalyzerPath; this.shouldUseCsharpEnterprise = shouldUseCsharpEnterprise; this.shouldUseVbNetEnterprise = shouldUseVbNetEnterprise; } @Nullable public Path getActualCsharpAnalyzerPath() { return actualCsharpAnalyzerPath; } public boolean isSupportsCsharp() { return supportsCsharp; } public boolean isSupportsVbNet() { return supportsVbNet; } public boolean isShouldUseCsharpEnterprise() { return shouldUseCsharpEnterprise; } public boolean isShouldUseVbNetEnterprise() { return shouldUseVbNetEnterprise; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginJarUtils.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInfo; public class PluginJarUtils { private static final SonarLintLogger LOG = SonarLintLogger.get(); private PluginJarUtils() { } @Nullable public static Version readVersion(Path jarPath) { try { return PluginInfo.create(jarPath).getVersion(); } catch (Exception e) { LOG.debug("Failed to read version from {}", jarPath, e); return null; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginLifecycleService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; /** * Coordinates plugin lifecycle operations and cache eviction. * This service sits at a higher architectural level than PluginsService, RulesRepository, * and ActiveRulesService, orchestrating operations across these services without creating * circular dependencies. */ public class PluginLifecycleService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final PluginsService pluginsService; private final RulesRepository rulesRepository; private final ActiveRulesService activeRulesService; public PluginLifecycleService(PluginsService pluginsService, RulesRepository rulesRepository, ActiveRulesService activeRulesService) { this.pluginsService = pluginsService; this.rulesRepository = rulesRepository; this.activeRulesService = activeRulesService; } public PluginsConfiguration reloadPluginsAndEvictCaches(String connectionId) { LOG.debug("Reloading plugins and evicting all related caches for connection '{}'", connectionId); unloadPluginsAndEvictCaches(connectionId); return pluginsService.getPlugins(connectionId); } public void unloadPluginsAndEvictCaches(String connectionId) { LOG.debug("Unloading plugins and evicting all related caches for connection '{}'", connectionId); pluginsService.unloadPlugins(connectionId); rulesRepository.evictFor(connectionId); activeRulesService.evictFor(connectionId); } public PluginsConfiguration reloadEmbeddedPluginsAndEvictCaches() { LOG.debug("Reloading embedded plugins and evicting all related caches"); unloadEmbeddedPluginsAndEvictCaches(); return pluginsService.getEmbeddedPlugins(); } public void unloadEmbeddedPluginsAndEvictCaches() { LOG.debug("Unloading embedded plugins and evicting all related caches"); pluginsService.unloadEmbeddedPlugins(); rulesRepository.evictEmbedded(); activeRulesService.evictStandalone(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginStatus.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; /** * @param pluginKey the plugin key * @param language language that this plugin provides analysis for * @param state current state of the plugin at the backend * @param source source where the plugin jar came from * @param actualVersion used version of the plugin * @param overriddenVersion a version of the plugin that is overridden by the actualVersion, if any * @param path path to the plugin jar on disk; populated for SYNCED/ACTIVE, null for DOWNLOADING/FAILED * @param serverVersion version of the SonarQube Server that provided this plugin; {@code null} for non-server sources */ public record PluginStatus( String pluginKey, @Nullable SonarLanguage language, ArtifactState state, @Nullable ArtifactOrigin source, @Nullable Version actualVersion, @Nullable Version overriddenVersion, @Nullable Path path, @Nullable String serverVersion) { public static PluginStatus forLanguage(SonarLanguage language, ArtifactState state, @Nullable ArtifactOrigin source, @Nullable Version actual, @Nullable Version overridden, @Nullable Path path, @Nullable String serverVersion) { return new PluginStatus(language.getPlugin().getKey(), language, state, source, actual, overridden, path, serverVersion); } public static PluginStatus forCompanion(String pluginKey, ArtifactState state, @Nullable ArtifactOrigin source, @Nullable Path path, @Nullable String serverVersion) { return new PluginStatus(pluginKey, null, state, source, null, null, path, serverVersion); } public static PluginStatus unsupported(SonarLanguage language) { return forLanguage(language, ArtifactState.UNSUPPORTED, null, null, null, null, null); } public static PluginStatus failed(SonarLanguage language) { return forLanguage(language, ArtifactState.FAILED, null, null, null, null, null); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginStatusMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.util.List; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.ArtifactSourceDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStateDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; public class PluginStatusMapper { private PluginStatusMapper() { } public static List toDto(List statuses) { return statuses.stream().map(PluginStatusMapper::toDto).toList(); } public static PluginStatusDto toDto(PluginStatus status) { return new PluginStatusDto( status.language() != null ? Language.valueOf(status.language().name()) : null, status.language() != null ? status.language().getName() : null, toDto(status.state()), toDto(status.source()), status.actualVersion() == null ? null : status.actualVersion().toString(), status.overriddenVersion() == null ? null : status.overriddenVersion().toString(), status.serverVersion() ); } public static PluginStateDto toDto(ArtifactState state) { return switch (state) { case ACTIVE -> PluginStateDto.ACTIVE; case SYNCED -> PluginStateDto.SYNCED; case DOWNLOADING -> PluginStateDto.DOWNLOADING; case FAILED -> PluginStateDto.FAILED; case PREMIUM -> PluginStateDto.PREMIUM; case UNSUPPORTED -> PluginStateDto.UNSUPPORTED; }; } @Nullable public static ArtifactSourceDto toDto(@Nullable ArtifactOrigin source) { if (source == null) { return null; } return switch (source) { case EMBEDDED -> ArtifactSourceDto.EMBEDDED; case ON_DEMAND -> ArtifactSourceDto.ON_DEMAND; case SONARQUBE_SERVER -> ArtifactSourceDto.SONARQUBE_SERVER; case SONARQUBE_CLOUD -> ArtifactSourceDto.SONARQUBE_CLOUD; }; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginStatusNotifierService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidChangePluginStatusesParams; import org.springframework.context.event.EventListener; public class PluginStatusNotifierService { private final PluginsService pluginsService; private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; public PluginStatusNotifierService(PluginsService pluginsService, SonarLintRpcClient client, ConfigurationRepository configurationRepository) { this.pluginsService = pluginsService; this.client = client; this.configurationRepository = configurationRepository; } @EventListener public void onPluginStatusesChanged(PluginStatusesChangedEvent event) { var connectionId = event.connectionId(); if (connectionId != null) { // All affected scopes share the same connection: reuse the pre-computed statuses from the event var statusDtos = PluginStatusMapper.toDto(event.pluginStatuses()); configurationRepository.getBoundScopesToConnection(connectionId).stream() .map(BoundScope::getConfigScopeId) .forEach(scopeId -> client.didChangePluginStatuses(new DidChangePluginStatusesParams(scopeId, statusDtos))); } else { // Embedded plugins changed: each scope may have a different effective connection, resolve per scope for (var configScopeId : configurationRepository.getConfigScopeIds()) { var effectiveConnectionId = configurationRepository.getEffectiveBinding(configScopeId) .map(Binding::connectionId).orElse(null); var newStatuses = pluginsService.getPluginStatuses(effectiveConnectionId); client.didChangePluginStatuses(new DidChangePluginStatusesParams(configScopeId, PluginStatusMapper.toDto(newStatuses))); } } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginStatusesChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.util.List; import javax.annotation.Nullable; /** * Published when the loaded state of plugins changes for a given connection (or for embedded plugins when connectionId is null). * Carries the pre-computed plugin statuses to avoid redundant resolver traversal in listeners. */ public record PluginStatusesChangedEvent(@Nullable String connectionId, List pluginStatuses) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginsConfiguration.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.util.Map; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ArtifactsLoadingResult; public record PluginsConfiguration(ArtifactsLoadingResult artifactsResult, LoadedPlugins plugins, Map extraProperties) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginsRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.CheckForNull; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.throwFirstWithOtherSuppressed; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.tryAndCollectIOException; public class PluginsRepository { private final AtomicReference embeddedPlugins = new AtomicReference<>(); private final Map pluginsByConnectionId = new HashMap<>(); public void setEmbeddedPlugins(PluginsConfiguration config) { this.embeddedPlugins.set(config); } @CheckForNull public PluginsConfiguration getEmbeddedPlugins() { return embeddedPlugins.get(); } @CheckForNull public PluginsConfiguration getPlugins(String connectionId) { return pluginsByConnectionId.get(connectionId); } public void setPlugins(String connectionId, PluginsConfiguration config) { pluginsByConnectionId.put(connectionId, config); } void unloadAllPlugins() throws IOException { Queue exceptions = new LinkedList<>(); var embedded = embeddedPlugins.get(); if (embedded != null) { tryAndCollectIOException(embedded.plugins()::close, exceptions); embeddedPlugins.set(null); } synchronized (pluginsByConnectionId) { pluginsByConnectionId.values().forEach(config -> tryAndCollectIOException(config.plugins()::close, exceptions)); pluginsByConnectionId.clear(); } throwFirstWithOtherSuppressed(exceptions); } public void unload(String connectionId) { var config = pluginsByConnectionId.remove(connectionId); if (config != null) { try { config.plugins().close(); } catch (IOException e) { throw new IllegalStateException("Unable to unload plugins", e); } } } public void unloadEmbedded() { var config = embeddedPlugins.getAndSet(null); if (config != null) { try { config.plugins().close(); } catch (IOException e) { throw new IllegalStateException("Unable to unload embedded plugins", e); } } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/PluginsService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import jakarta.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.commons.PluginsLoader; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginRequirementsCheckResult; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ArtifactsLoadingResult; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ArtifactsLoadingStrategy; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ConnectedArtifactsLoadingStrategyFactory; import org.sonarsource.sonarlint.core.plugin.loading.strategy.StandaloneArtifactsLoadingStrategy; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPlugin; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsRepository; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.PluginsSynchronizedEvent; import org.springframework.context.ApplicationEventPublisher; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.DATAFLOW_BUG_DETECTION; public class PluginsService { private static final Version REPACKAGED_DOTNET_ANALYZER_MIN_SQ_VERSION = Version.create("10.8"); public static final String CSHARP_ENTERPRISE_PLUGIN_ID = "csharpenterprise"; public static final String VBNET_ENTERPRISE_PLUGIN_ID = "vbnetenterprise"; private final SonarLintLogger logger = SonarLintLogger.get(); private final PluginsRepository pluginsRepository; private final SkippedPluginsRepository skippedPluginsRepository; private final StorageService storageService; private final InitializeParams initializeParams; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final NodeJsService nodeJsService; private final boolean enableDataflowBugDetection; private final ApplicationEventPublisher eventPublisher; private final StandaloneArtifactsLoadingStrategy standaloneArtifactsLoadingStrategy; private final ConnectedArtifactsLoadingStrategyFactory connectedArtifactsLoadingStrategyFactory; private final BinariesArtifactSource binariesArtifactSource; public PluginsService(PluginsRepository pluginsRepository, SkippedPluginsRepository skippedPluginsRepository, StorageService storageService, InitializeParams params, ConnectionConfigurationRepository connectionConfigurationRepository, NodeJsService nodeJsService, ApplicationEventPublisher eventPublisher, StandaloneArtifactsLoadingStrategy standaloneArtifactsLoadingStrategy, ConnectedArtifactsLoadingStrategyFactory connectedArtifactsLoadingStrategyFactory, BinariesArtifactSource binariesArtifactSource) { this.pluginsRepository = pluginsRepository; this.skippedPluginsRepository = skippedPluginsRepository; this.storageService = storageService; this.enableDataflowBugDetection = params.getBackendCapabilities().contains(DATAFLOW_BUG_DETECTION); this.initializeParams = params; this.connectionConfigurationRepository = connectionConfigurationRepository; this.nodeJsService = nodeJsService; this.eventPublisher = eventPublisher; this.standaloneArtifactsLoadingStrategy = standaloneArtifactsLoadingStrategy; this.connectedArtifactsLoadingStrategyFactory = connectedArtifactsLoadingStrategyFactory; this.binariesArtifactSource = binariesArtifactSource; } public List getPluginStatuses(@Nullable String connectionId) { var plugins = connectionId == null ? getEmbeddedPlugins() : getPlugins(connectionId); return getPluginStatuses(plugins.artifactsResult()); } private static List getPluginStatuses(ArtifactsLoadingResult result) { return Arrays.stream(SonarLanguage.values()) .map(language -> buildPluginStatus(language, result)) .toList(); } private static PluginStatus buildPluginStatus(SonarLanguage language, ArtifactsLoadingResult result) { var pluginKey = resolvePluginKey(language, result.resolvedArtifactsByKey()); return result.getResolvedArtifactByKey(pluginKey) .map(artifact -> PluginStatus.forLanguage(language, artifact.state(), artifact.source(), artifact.version(), null, artifact.path(), null)) .orElseGet(() -> PluginStatus.unsupported(language)); } private ArtifactsLoadingStrategy getPluginLoadingStrategy(@Nullable String connectionId) { return connectionId != null ? connectedArtifactsLoadingStrategyFactory.getOrCreate(connectionId) : standaloneArtifactsLoadingStrategy; } /** * Returns the effective plugin key for a language, preferring the enterprise variant if it is * already present in the resolved map. */ private static String resolvePluginKey(SonarLanguage language, Map resolved) { var baseKey = language.getPlugin().getKey(); var enterpriseKeys = SonarPlugin.findByKey(baseKey) .map(SonarPlugin::getEnterpriseVariants) .map(variants -> variants.stream().map(SonarPlugin::getKey).collect(Collectors.toSet())) .orElseGet(Set::of); return enterpriseKeys.stream() .filter(resolved::containsKey) .findFirst() .orElse(baseKey); } public PluginsConfiguration getEmbeddedPlugins() { var cached = pluginsRepository.getEmbeddedPlugins(); if (cached == null) { cached = loadPlugins(null); pluginsRepository.setEmbeddedPlugins(cached); eventPublisher.publishEvent(new PluginStatusesChangedEvent(null, getPluginStatuses(cached.artifactsResult()))); } return cached; } public PluginsConfiguration getPlugins(String connectionId) { var cached = pluginsRepository.getPlugins(connectionId); if (cached == null) { cached = loadPlugins(connectionId); pluginsRepository.setPlugins(connectionId, cached); eventPublisher.publishEvent(new PluginStatusesChangedEvent(connectionId, getPluginStatuses(cached.artifactsResult()))); } return cached; } private PluginsConfiguration loadPlugins(@Nullable String connectionId) { var strategy = getPluginLoadingStrategy(connectionId); var artifactsResult = strategy.resolveArtifacts(); artifactsResult.whenAllArtifactsDownloaded(() -> eventPublisher.publishEvent(new PluginsSynchronizedEvent(connectionId))); var config = new PluginsLoader.Configuration(new HashSet<>(artifactsResult.getPluginPaths()), artifactsResult.enabledLanguages(), enableDataflowBugDetection, nodeJsService.getActiveNodeJsVersion()); var pluginsLoadResult = new PluginsLoader().load(config, initializeParams.getDisabledPluginKeysForAnalysis()); var skippedPlugins = pluginsLoadResult.getPluginCheckResultByKeys().values().stream() .filter(PluginRequirementsCheckResult::isSkipped) .map(plugin -> new SkippedPlugin(plugin.getPlugin().getKey(), plugin.getSkipReason().get())) .toList(); if (connectionId == null) { skippedPluginsRepository.setSkippedEmbeddedPlugins(skippedPlugins); } else { skippedPluginsRepository.setSkippedPlugins(connectionId, skippedPlugins); } return new PluginsConfiguration(artifactsResult, pluginsLoadResult.getLoadedPlugins(), buildExtraProperties(connectionId, artifactsResult)); } private Map buildExtraProperties(@Nullable String connectionId, ArtifactsLoadingResult result) { var properties = new HashMap(); var dotnetSupport = getDotnetSupport(connectionId, result); if (dotnetSupport.getActualCsharpAnalyzerPath() != null) { properties.put("sonar.cs.internal.analyzerPath", dotnetSupport.getActualCsharpAnalyzerPath().toString()); } if (dotnetSupport.isSupportsCsharp()) { properties.put("sonar.cs.internal.shouldUseCsharpEnterprise", String.valueOf(dotnetSupport.isShouldUseCsharpEnterprise())); } if (dotnetSupport.isSupportsVbNet()) { properties.put("sonar.cs.internal.shouldUseVbEnterprise", String.valueOf(dotnetSupport.isShouldUseVbNetEnterprise())); } properties.putAll(binariesArtifactSource.getOmnisharpExtraProperties()); return properties; } public void unloadPlugins(String connectionId) { logger.debug("Evict loaded plugins for connection '{}'", connectionId); pluginsRepository.unload(connectionId); connectedArtifactsLoadingStrategyFactory.evict(connectionId); } public boolean shouldUseEnterpriseCSharpAnalyzer(String connectionId) { return shouldUseEnterpriseDotNetAnalyzer(connectionId, CSHARP_ENTERPRISE_PLUGIN_ID); } private boolean shouldUseEnterpriseDotNetAnalyzer(String connectionId, String analyzerName) { if (isSonarQubeCloud(connectionId)) { return true; } else { var connectionStorage = storageService.connection(connectionId); var serverInfo = connectionStorage.serverInfo().read(); if (serverInfo.isEmpty()) { return false; } else { var serverVersion = serverInfo.get().version(); var supportsRepackagedDotnetAnalyzer = serverVersion.compareToIgnoreQualifier(REPACKAGED_DOTNET_ANALYZER_MIN_SQ_VERSION) >= 0; var hasEnterprisePlugin = connectionStorage.plugins().getStoredPlugins().stream().map(StoredPlugin::getKey).anyMatch(analyzerName::equals); return !supportsRepackagedDotnetAnalyzer || hasEnterprisePlugin; } } } private boolean isSonarQubeCloud(String connectionId) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); return connection != null && connection.getKind() == ConnectionKind.SONARCLOUD; } public boolean shouldUseEnterpriseVbAnalyzer(String connectionId) { return shouldUseEnterpriseDotNetAnalyzer(connectionId, VBNET_ENTERPRISE_PLUGIN_ID); } private DotnetSupport getDotnetSupport(@Nullable String connectionId, ArtifactsLoadingResult result) { var ossPath = resolveOssCsharpAnalyzerPath(result); if (connectionId == null) { return new DotnetSupport(initializeParams, ossPath, false, false); } var useEnterpriseCs = shouldUseEnterpriseCSharpAnalyzer(connectionId); var useEnterpriseVb = shouldUseEnterpriseVbAnalyzer(connectionId); var actualPath = selectCsharpAnalyzerPath(connectionId, ossPath, useEnterpriseCs); return new DotnetSupport(initializeParams, actualPath, useEnterpriseCs, useEnterpriseVb); } @Nullable private static Path resolveOssCsharpAnalyzerPath(ArtifactsLoadingResult result) { return result.getResolvedArtifactByKey(SonarPlugin.CS_OSS.getKey()) .map(ResolvedArtifact::path) .orElse(null); } @Nullable private Path selectCsharpAnalyzerPath(String connectionId, @Nullable Path ossPath, boolean useEnterprise) { if (useEnterprise) { return getStoredEnterprisePath(connectionId).orElse(ossPath); } return ossPath; } private Optional getStoredEnterprisePath(String connectionId) { return Optional.ofNullable(storageService.connection(connectionId).plugins().getStoredPluginsByKey().get(CSHARP_ENTERPRISE_PLUGIN_ID)) .map(StoredPlugin::getJarPath); } public void unloadEmbeddedPlugins() { logger.debug("Evict loaded embedded plugins"); pluginsRepository.unloadEmbedded(); } @PreDestroy public void shutdown() throws IOException { try { pluginsRepository.unloadAllPlugins(); } catch (Exception e) { SonarLintLogger.get().error("Error shutting down plugins service", e); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ArtifactCandidate.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; /** * Pairs an {@link AvailableArtifact} with the {@link ArtifactSource} that won the priority * contest for its key. Used as the value type of the winner-map in both loading strategies. */ record ArtifactCandidate(AvailableArtifact available, ArtifactSource source) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ArtifactsLoadingResult.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; public record ArtifactsLoadingResult(Set enabledLanguages, Map resolvedArtifactsByKey) { public Optional getResolvedArtifactByKey(String key) { return Optional.ofNullable(resolvedArtifactsByKey().get(key)); } /** * All artifacts must not be loaded, only real Sonar plugins. */ public List getPluginPaths() { var pluginPaths = new ArrayList(); for (var entry : resolvedArtifactsByKey.entrySet()) { var key = entry.getKey(); var artifact = entry.getValue(); // only load artifacts that are ready on disk and not dependencies if (artifact == null || artifact.path() == null || SonarPluginDependency.findByKey(key).isPresent()) { continue; } // only load plugins whose required dependencies are also present on disk if (areRequiredDependenciesPresent(key)) { pluginPaths.add(artifact.path()); } } return pluginPaths; } private boolean areRequiredDependenciesPresent(String key) { return SonarPlugin.findByKey(key) .map(plugin -> plugin.getDependencies().stream() .filter(dep -> !dep.optional()) .allMatch(dep -> { var depArtifact = resolvedArtifactsByKey.get(dep.artifact().getKey()); return depArtifact != null && depArtifact.path() != null; })) .orElse(true); } public Optional> getAllDownloadsFuture() { var pendingDownloads = resolvedArtifactsByKey.values().stream().map(ResolvedArtifact::downloadFuture) .filter(Objects::nonNull) .toList(); if (pendingDownloads.isEmpty()) { return Optional.empty(); } return Optional.of(CompletableFuture.allOf(pendingDownloads.toArray(new CompletableFuture[0]))); } public void whenAllArtifactsDownloaded(Runnable runnable) { getAllDownloadsFuture() .ifPresent(future -> { var logOutput = SonarLintLogger.get().getTargetForCopy(); future.thenRun(() -> { SonarLintLogger.get().setTarget(logOutput); runnable.run(); }); }); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ArtifactsLoadingStrategy.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; /** * Defines how {@link org.sonarsource.sonarlint.core.plugin.source.ArtifactSource ArtifactSource} * instances are combined to produce the full set of resolved artifacts for a given context. * *

There are two implementations: *

    *
  • {@link StandaloneArtifactsLoadingStrategy} — standalone mode, no server connection.
  • *
  • {@link ConnectedArtifactsLoadingStrategy} — connected mode, one instance per connection.
  • *
* *

Consumed by {@link PluginsService} to resolve artifacts without knowing the mode. * All complexity of listing sources, prioritizing, applying skip-lists, handling enterprise * variants and companion plugins stays hidden inside the implementation.

*/ public interface ArtifactsLoadingStrategy { /** * Resolves all artifacts (plugins and plugin dependencies) from all managed sources. * Higher-priority sources overwrite lower-priority ones for the same key. * May schedule background downloads; entries with a {@code null} path are still being fetched. * * @return a map from artifact key to its current {@link ResolvedArtifact} */ ArtifactsLoadingResult resolveArtifacts(); } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/BaseArtifactsLoadingStrategy.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.util.Map; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; /** * Base class for artifact loading strategies. Provides shared filter passes that apply to both * standalone and connected mode. * *

Subclasses build a {@code LinkedHashMap} winner-map using * their source-specific priority rules, then call {@link #removeOrphanDependencies} and * {@link #removeMissingRequiredDeps} before loading artifacts.

*/ abstract class BaseArtifactsLoadingStrategy implements ArtifactsLoadingStrategy { protected BaseArtifactsLoadingStrategy() { // only instantiable from subclasses } /** * Removes dependency artifacts whose dependent plugin is not present in the candidate map. * *

A {@link SonarPluginDependency} with no corresponding dependent {@link SonarPlugin} * in the map is an orphan and must not be loaded.

*/ protected static void removeOrphanDependencies(Map candidates) { candidates.entrySet().removeIf(e -> { var sonarArtifact = e.getValue().available().sonarArtifact(); return sonarArtifact.isPresent() && sonarArtifact.get() instanceof SonarPluginDependency dependency && dependency.getDependents().stream().noneMatch(p -> candidates.containsKey(p.getKey())); }); } /** * Removes plugins that are missing at least one required (non-optional) dependency in the * candidate map. */ protected static void removeMissingRequiredDeps(Map candidates) { candidates.entrySet().removeIf(e -> { var sonarArtifact = e.getValue().available().sonarArtifact(); return sonarArtifact.isPresent() && sonarArtifact.get() instanceof SonarPlugin plugin && plugin.getDependencies().stream() .filter(dep -> !dep.optional()) .anyMatch(dep -> !candidates.containsKey(dep.artifact().getKey())); }); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ConnectedArtifactsLoadingStrategy.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.embedded.EmbeddedPluginSource; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; /** * Artifacts loading strategy for connected mode (a specific connection). * *

One instance is created per connection and cached by * {@link ConnectedArtifactsLoadingStrategyFactory}.

* *

Sources, in ascending priority order: *

    *
  1. {@link BinariesArtifactSource} — on-demand downloadable artifacts (fallback).
  2. *
  3. {@link ServerPluginSource} — artifacts synced from the server.
  4. *
  5. {@link EmbeddedPluginSource} (connected) — JARs embedded in the IDE client (highest * priority in normal circumstances).
  6. *
* *

{@link #resolveArtifacts()} uses a winner-map pattern: iterate sources in ascending * priority, last writer wins per key, then apply passes to correct the map before loading. * *

Connected-mode-specific passes (applied before the shared passes): *

    *
  1. Enterprise-variant deduplication: when a different-key enterprise variant * ({@code csharpenterprise}, {@code vbnetenterprise}) is present, the base key is removed * so both are not loaded simultaneously.
  2. *
  3. Enterprise priority override: when the server reports a plugin as enterprise * ({@link AvailableArtifact#isEnterprise()}), that plugin is forced to use the server * source even if the embedded source would normally win. This applies to same-key * enterprise plugins (GO, IAC) whose enterprise edition is served when the * connection qualifies (SonarQube Server ≥ minimum version, or SonarQube Cloud).
  4. *
*/ public class ConnectedArtifactsLoadingStrategy extends BaseArtifactsLoadingStrategy { private final ServerPluginSource serverSource; private final LanguageSupportRepository languageSupportRepository; private final List artifactSourcesSortedByAscendingPriority; ConnectedArtifactsLoadingStrategy(InitializeParams params, BinariesArtifactSource binariesSource, ServerPluginSource serverSource, LanguageSupportRepository languageSupportRepository) { this.serverSource = serverSource; this.languageSupportRepository = languageSupportRepository; // Ascending priority: binaries (fallback) → server → embedded (highest) this.artifactSourcesSortedByAscendingPriority = List.of( binariesSource, serverSource, EmbeddedPluginSource.forConnected(params)); } /** * Resolves all artifacts from all sources using a winner-map pattern. May schedule background * downloads. * *

Priority (highest wins in normal cases): embedded > server > binaries. * Exception: enterprise server plugins beat embedded (see class Javadoc).

*/ @Override public ArtifactsLoadingResult resolveArtifacts() { var enabledLanguages = languageSupportRepository.getEnabledLanguagesInConnectedMode(); // Query server artifacts once; reused in normal pass and enterprise-override pass var serverArtifacts = serverSource.listAvailableArtifacts(enabledLanguages); // Winner-map: ascending priority, last writer wins per key var candidates = new LinkedHashMap(); for (var source : artifactSourcesSortedByAscendingPriority) { var artifacts = (source == serverSource) ? serverArtifacts : source.listAvailableArtifacts(enabledLanguages); for (var artifact : artifacts) { candidates.put(artifact.key(), new ArtifactCandidate(artifact, source)); } } // Pass 1 (connected-specific): remove base keys superseded by a different-key enterprise variant new ArrayList<>(candidates.keySet()).stream() .filter(SonarPlugin::isEnterpriseVariant) .forEach(entKey -> SonarPlugin.baseKeyFor(entKey).ifPresent(candidates::remove)); // Pass 2 (connected-specific): enterprise server plugins override even embedded serverArtifacts.stream() .filter(AvailableArtifact::isEnterprise) .forEach(a -> candidates.computeIfPresent(a.key(), (k, existing) -> new ArtifactCandidate(existing.available(), serverSource))); // Shared passes removeOrphanDependencies(candidates); removeMissingRequiredDeps(candidates); // Group winning keys by source, then load once per source. // Pre-populate all sources with empty sets so every source is always called (e.g. ServerPluginSource // needs to be called even with an empty set to initialize its storage when nothing is downloaded). var keysBySource = new HashMap>(); for (var source : artifactSourcesSortedByAscendingPriority) { keysBySource.put(source, new HashSet<>()); } candidates.forEach((key, candidate) -> keysBySource.get(candidate.source()).add(key)); var result = new LinkedHashMap(); keysBySource.forEach((source, keys) -> result.putAll(source.load(keys).resolvedArtifactsByKey())); return new ArtifactsLoadingResult(enabledLanguages, result); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ConnectedArtifactsLoadingStrategyFactory.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginsCache; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginDownloader; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.storage.StorageService; /** * Creates and caches {@link ConnectedArtifactsLoadingStrategy} instances, one per connection ID. * *

The cache ensures that the same strategy — and its underlying * {@link ServerPluginSource} — is reused across calls for the same connection, which is required * for consistent in-progress download tracking. Call {@link #evict(String)} when a connection is * removed.

*/ public class ConnectedArtifactsLoadingStrategyFactory { private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); private final InitializeParams params; private final BinariesArtifactSource binariesSource; private final StorageService storageService; private final ServerPluginsCache serverPluginsCache; private final ServerPluginDownloader downloader; private final LanguageSupportRepository languageSupportRepository; public ConnectedArtifactsLoadingStrategyFactory(InitializeParams params, BinariesArtifactSource binariesSource, StorageService storageService, ServerPluginsCache serverPluginsCache, ServerPluginDownloader downloader, LanguageSupportRepository languageSupportRepository) { this.params = params; this.binariesSource = binariesSource; this.storageService = storageService; this.serverPluginsCache = serverPluginsCache; this.downloader = downloader; this.languageSupportRepository = languageSupportRepository; } public ConnectedArtifactsLoadingStrategy getOrCreate(String connectionId) { return cache.computeIfAbsent(connectionId, id -> { var serverSource = new ServerPluginSource(id, storageService, serverPluginsCache, downloader); return new ConnectedArtifactsLoadingStrategy(params, binariesSource, serverSource, languageSupportRepository); }); } public void evict(String connectionId) { cache.remove(connectionId); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/StandaloneArtifactsLoadingStrategy.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.embedded.EmbeddedPluginSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; /** * Artifacts loading strategy for standalone (no-connection) mode. * *

Sources, in ascending priority order: *

    *
  1. {@link BinariesArtifactSource} — on-demand downloadable artifacts.
  2. *
  3. {@link EmbeddedPluginSource} (standalone) — JARs embedded in the IDE client.
  4. *
* *

Languages available only in connected mode are reported as * {@link ArtifactState#PREMIUM} when no other source can provide them.

*/ public class StandaloneArtifactsLoadingStrategy extends BaseArtifactsLoadingStrategy { private final InitializeParams params; private final BinariesArtifactSource binariesSource; private final LanguageSupportRepository languageSupportRepository; @Nullable private List artifactSourcesSortedByAscendingPriority; public StandaloneArtifactsLoadingStrategy(InitializeParams params, BinariesArtifactSource binariesSource, LanguageSupportRepository languageSupportRepository) { this.params = params; this.binariesSource = binariesSource; this.languageSupportRepository = languageSupportRepository; } private List getArtifactSourcesByAscendingPriority() { if (artifactSourcesSortedByAscendingPriority == null) { // Ascending priority: binaries < embedded. Later source overwrites for the same key. // EmbeddedPluginSource.forStandalone reads JAR manifests and may throw — defer until first use. artifactSourcesSortedByAscendingPriority = List.of(binariesSource, EmbeddedPluginSource.forStandalone(params)); } return artifactSourcesSortedByAscendingPriority; } /** * Resolves all artifacts from standalone sources. * *

Priority (highest wins): embedded over binaries. Connected-only languages that * cannot be provided by either source are reported as {@link ArtifactState#PREMIUM}.

*/ @Override public ArtifactsLoadingResult resolveArtifacts() { var enabledLanguages = languageSupportRepository.getEnabledLanguagesInStandaloneMode(); // Winner-map: ascending priority, last writer wins per key var candidates = new LinkedHashMap(); for (var source : getArtifactSourcesByAscendingPriority()) { for (var artifact : source.listAvailableArtifacts(enabledLanguages)) { candidates.put(artifact.key(), new ArtifactCandidate(artifact, source)); } } // remove base keys superseded by a different-key enterprise variant new ArrayList<>(candidates.keySet()).stream() .filter(SonarPlugin::isEnterpriseVariant) .forEach(entKey -> SonarPlugin.baseKeyFor(entKey).ifPresent(candidates::remove)); removeOrphanDependencies(candidates); removeMissingRequiredDeps(candidates); // Group winning keys by source, then load once per source var keysBySource = new HashMap>(); candidates.forEach((key, candidate) -> keysBySource.computeIfAbsent(candidate.source(), s -> new HashSet<>()).add(key)); var result = new LinkedHashMap(); keysBySource.forEach((source, keys) -> result.putAll(source.load(keys).resolvedArtifactsByKey())); // For each language not yet resolved and available only in connected mode, mark PREMIUM for (var language : SonarLanguage.values()) { var key = language.getPlugin().getKey(); if (!result.containsKey(key) && languageSupportRepository.isEnabledOnlyInConnectedMode(language)) { result.put(key, ResolvedArtifact.premium()); } } return new ArtifactsLoadingResult(enabledLanguages, result); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.loading.strategy; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPlugin.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.skipped; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason; public class SkippedPlugin { private final String key; private final SkipReason reason; public SkippedPlugin(String key, SkipReason skipReason) { this.key = key; this.reason = skipReason; } public String getKey() { return key; } public SkipReason getReason() { return reason; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsNotifierService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.skipped; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.analysis.AnalysisFinishedEvent; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidSkipLoadingPluginParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.springframework.context.event.EventListener; public class SkippedPluginsNotifierService { private final SkippedPluginsRepository skippedPluginsRepository; private final ConfigurationRepository configurationRepository; private final SonarLintRpcClient client; private final Set alreadyNotifiedPluginKeys = new HashSet<>(); public SkippedPluginsNotifierService(SkippedPluginsRepository skippedPluginsRepository, ConfigurationRepository configurationRepository, SonarLintRpcClient client) { this.skippedPluginsRepository = skippedPluginsRepository; this.configurationRepository = configurationRepository; this.client = client; } @EventListener public void onAnalysisFinished(AnalysisFinishedEvent event) { var detectedLanguages = event.getDetectedLanguages(); var configurationScopeId = event.getConfigurationScopeId(); var skippedPlugins = getSkippedPluginsToNotify(configurationScopeId); if (skippedPlugins.isEmpty()) { return; } notifyClientOfSkippedPlugins(configurationScopeId, detectedLanguages, skippedPlugins); } private void notifyClientOfSkippedPlugins(String configurationScopeId, Set detectedLanguages, List skippedPlugins) { detectedLanguages.stream().filter(Objects::nonNull) .forEach(sonarLanguage -> skippedPlugins.stream().filter(p -> p.getKey().equals(sonarLanguage.getPlugin().getKey())) .findFirst() .ifPresent(skippedPlugin -> { var skipReason = skippedPlugin.getReason(); if (skipReason instanceof SkipReason.UnsatisfiedRuntimeRequirement runtimeRequirement) { var rpcLanguage = Language.valueOf(sonarLanguage.name()); var rpcSkipReason = runtimeRequirement.getRuntime() == SkipReason.UnsatisfiedRuntimeRequirement.RuntimeRequirement.JRE ? DidSkipLoadingPluginParams.SkipReason.UNSATISFIED_JRE : DidSkipLoadingPluginParams.SkipReason.UNSATISFIED_NODE_JS; alreadyNotifiedPluginKeys.add(skippedPlugin.getKey()); client.didSkipLoadingPlugin( new DidSkipLoadingPluginParams(configurationScopeId, rpcLanguage, rpcSkipReason, runtimeRequirement.getMinVersion(), runtimeRequirement.getCurrentVersion())); } })); } private List getSkippedPluginsToNotify(String configurationScopeId) { var skippedPlugins = getSkippedPlugins(configurationScopeId); if (skippedPlugins != null) { return skippedPlugins.stream().filter(skippedPlugin -> !alreadyNotifiedPluginKeys.contains(skippedPlugin.getKey())).toList(); } return List.of(); } @CheckForNull private List getSkippedPlugins(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId) .map(binding -> skippedPluginsRepository.getSkippedPlugins(binding.connectionId())) .orElseGet(skippedPluginsRepository::getSkippedEmbeddedPlugins); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/SkippedPluginsRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.skipped; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; public class SkippedPluginsRepository { private List skippedEmbeddedPlugins; private final Map> skippedPluginsByConnectionId = new HashMap<>(); public void setSkippedEmbeddedPlugins(List skippedPlugins) { this.skippedEmbeddedPlugins = skippedPlugins; } @CheckForNull public List getSkippedEmbeddedPlugins() { return skippedEmbeddedPlugins; } public List getSkippedPlugins(String connectionId) { return skippedPluginsByConnectionId.get(connectionId); } public void setSkippedPlugins(String connectionId, List skippedPlugins) { skippedPluginsByConnectionId.put(connectionId, skippedPlugins); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/skipped/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.skipped; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/ArtifactKind.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import org.sonarsource.sonarlint.core.plugin.PluginStatus; /** * Identifies where an artifact physically came from. * Used in {@link ResolvedArtifact} and {@link PluginStatus} to convey provenance. * * @see ArtifactSource the interface representing the provider of artifacts */ public enum ArtifactKind { PLUGIN, DEPENDENCY } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/ArtifactOrigin.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import org.sonarsource.sonarlint.core.plugin.PluginStatus; /** * Identifies where an artifact physically came from. * Used in {@link ResolvedArtifact} and {@link PluginStatus} to convey provenance. * * @see ArtifactSource the interface representing the provider of artifacts */ public enum ArtifactOrigin { /** Bundled inside the IDE extension distribution. */ EMBEDDED, /** Downloaded on demand from binaries.sonarsource.com. */ ON_DEMAND, /** Synchronized from a SonarQube Server connection. */ SONARQUBE_SERVER, /** Synchronized from a SonarQube Cloud connection. */ SONARQUBE_CLOUD } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/ArtifactSource.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import java.util.List; import java.util.Set; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.embedded.EmbeddedPluginSource; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource; /** * Represents one of the origins from which artifacts (plugins and plugin dependencies) can be * obtained: the client (embedded), public binaries on-demand, or a SonarQube/SonarQube Cloud * server (connected mode). * *

There are three concrete implementations: *

    *
  • {@link EmbeddedPluginSource} — JARs bundled in the IDE extension.
  • *
  • {@link BinariesArtifactSource} — artifacts downloadable from * binaries.sonarsource.com.
  • *
  • {@link ServerPluginSource} — artifacts synchronized from a connected server.
  • *
* *

The two methods follow a list-then-act pattern: *

    *
  • {@link #listAvailableArtifacts(Set)} is a pure query — no side effects, no downloads.
  • *
  • {@link #load(Set)} is the action — given the full set of artifact keys that this source * won in the priority contest, it ensures each artifact is available, scheduling background * downloads when necessary. Receiving the complete set at once allows implementations (in * particular {@link ServerPluginSource}) to take storage-level actions that require knowing * all winners upfront (e.g. writing empty reference files for server plugins that were not * selected). Keys absent from the returned {@link LoadResult} are silently ignored by the * caller.
  • *
*/ public interface ArtifactSource { /** * Returns all artifacts known to this source for the given set of enabled languages, without triggering any downloads. This is a pure query. * Implementations should return artifacts corresponding to enabled languages, and artifacts that are not tied to a specific language. */ List listAvailableArtifacts(Set enabledLanguages); /** * Ensures every artifact in {@code artifactKeys} is available from this source, scheduling * background downloads when necessary. {@code artifactKeys} is the complete set of keys that * this source won in the priority contest for the current load cycle, allowing implementations * to reason about the full picture at once. A key may be absent from the returned * {@link LoadResult} if this source cannot provide it. Resolved artifacts may carry the state * {@link ArtifactState#DOWNLOADING} when a background download has been scheduled. */ LoadResult load(Set artifactKeys); } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/ArtifactState.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; public enum ArtifactState { ACTIVE("Active"), SYNCED("Synced"), DOWNLOADING("Downloading…"), FAILED("Failed"), PREMIUM("Premium"), UNSUPPORTED("Unsupported"); private final String name; ArtifactState(String name) { this.name = name; } public String getName() { return name; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/AvailableArtifact.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.plugins.SonarArtifact; /** * An artifact (plugin or plugin dependency) known to a given {@link ArtifactSource}. * Returned by {@link ArtifactSource#listAvailableArtifacts(Set)} as a pure query with no side * effects. * *

{@code isEnterprise} is {@code true} when the artifact is the enterprise edition of a * plugin on the current connection. Enterprise artifacts take priority over embedded sources * in {@code ConnectedArtifactsLoadingStrategy}.

*/ public record AvailableArtifact(String key, @Nullable Version version, boolean isEnterprise, Optional sonarArtifact) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/LoadResult.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import java.util.Map; /** * The result of a batch {@link ArtifactSource#load(java.util.Set)} call. * *

Wraps the resolved artifacts by key. Using a dedicated type instead of a raw map leaves * room for future fields (e.g. the set of artifacts that were available from this source but * not selected as winners, needed by {@code ServerPluginSource} to write empty reference files).

*/ public record LoadResult(Map resolvedArtifactsByKey) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/ResolvedArtifact.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; public record ResolvedArtifact(ArtifactState state, @Nullable Path path, @Nullable ArtifactOrigin source, @Nullable Version version, @Nullable CompletableFuture downloadFuture) { public static ResolvedArtifact premium() { return new ResolvedArtifact(ArtifactState.PREMIUM, null, null, null, null); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/UniqueTaskExecutor.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class UniqueTaskExecutor { private final Map> inProgress = new ConcurrentHashMap<>(); private final ExecutorService executor; public UniqueTaskExecutor(ExecutorService executor) { this.executor = executor; } public CompletableFuture scheduleIfAbsent(String key, Runnable task) { return inProgress.computeIfAbsent(key, k -> { var logOutput = SonarLintLogger.get().getTargetForCopy(); return CompletableFuture.runAsync(() -> { SonarLintLogger.get().setTarget(logOutput); try { task.run(); } finally { inProgress.remove(key); SonarLintLogger.get().setTarget(null); } }, executor); }); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesArtifact.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.IOException; import java.util.Arrays; import java.util.Optional; import java.util.Properties; import java.util.Set; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.plugins.SonarArtifact; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; @SuppressWarnings("java:S1192") public enum BinariesArtifact { CFAMILY_PLUGIN(SonarPlugin.C_FAMILY, "cfamily.version", "/CommercialDistribution/sonar-cfamily-plugin/sonar-cfamily-plugin-%s.jar", "ondemand/sonar-cpp-plugin.jar.asc"), CSHARP_OSS(SonarPlugin.CS_OSS, "cs.version", "/Distribution/sonar-csharp-plugin/sonar-csharp-plugin-%s.jar", "ondemand/sonar-cs-plugin.jar.asc"), OMNISHARP_MONO(SonarPluginDependency.OMNISHARP_MONO, "omnisharp.version", "/OmniSharp-Roslyn/%s/omnisharp-mono.tar.gz", "ondemand/omnisharp-mono.tar.gz.asc"), OMNISHARP_NET472(SonarPluginDependency.OMNISHARP_NET472, "omnisharp.version", "/OmniSharp-Roslyn/%s/omnisharp-net472.tar.gz", "ondemand/omnisharp-net472.tar.gz.asc"), OMNISHARP_NET6(SonarPluginDependency.OMNISHARP_NET6, "omnisharp.version", "/OmniSharp-Roslyn/%s/omnisharp-net6.0.tar.gz", "ondemand/omnisharp-net6.0.tar.gz.asc"); /** System property to override the download URL pattern for all artifacts, e.g. for testing with a mock server. */ public static final String PROPERTY_URL_PATTERN = "sonarlint.ondemand.url"; private static final String PROPERTIES_FILE = "ondemand/plugins.properties"; private static final String BINARIES_URL = "https://binaries.sonarsource.com"; private static final Properties VERSIONS = loadVersions(); private static final String TAR_GZ_EXTENSION = ".tar.gz"; private final SonarArtifact artifact; private final String versionKey; private final String urlPattern; private final String signatureResourcePath; BinariesArtifact(SonarArtifact artifact, String versionKey, String urlPattern, String signatureResourcePath) { this.artifact = artifact; this.versionKey = versionKey; this.urlPattern = urlPattern; this.signatureResourcePath = signatureResourcePath; } public static Optional findByKey(@Nullable String key) { return Arrays.stream(values()).filter(a -> a.artifactKey().equals(key)).findFirst(); } public String version() { if (!VERSIONS.containsKey(versionKey)) { throw new IllegalStateException("Version is not set in properties for " + artifactKey()); } return VERSIONS.getProperty(versionKey); } public String urlPattern() { var base = System.getProperty(PROPERTY_URL_PATTERN, BINARIES_URL); return base + urlPattern; } public String artifactKey() { return artifact.getKey(); } public String signatureResourcePath() { return signatureResourcePath; } public Set getLanguages() { return artifact.getLanguages(); } public boolean isArchive() { return urlPattern.endsWith(TAR_GZ_EXTENSION); } private static Properties loadVersions() { try (var input = BinariesArtifact.class.getClassLoader().getResourceAsStream(PROPERTIES_FILE)) { if (input == null) { throw new IllegalStateException("Unable to find " + PROPERTIES_FILE + " on classpath"); } var properties = new Properties(); properties.load(input); return properties; } catch (IOException e) { throw new IllegalStateException("Error loading plugin versions from " + PROPERTIES_FILE, e); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesArtifactSource.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.IOException; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarArtifact; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; import org.sonarsource.sonarlint.core.event.PluginStatusUpdateEvent; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.plugin.PluginStatus; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.UniqueTaskExecutor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import static org.sonarsource.sonarlint.core.serverconnection.storage.TarGzUtils.extractTarGz; /** * Artifact source backed by publicly downloadable artifacts from binaries.sonarsource.com. * Handles both plugins (CFamily, C# OSS) and plugin dependencies (OmniSharp distributions). * *

{@link #listAvailableArtifacts(Set)} is a pure query: it returns all known artifacts, with a * non-null {@code jarPath} only for those already cached and verified on disk. {@link #load(String)} * schedules a background download when the artifact is not yet cached, returning * {@link ArtifactState#DOWNLOADING} immediately. A {@link PluginStatusUpdateEvent} is published * when the download completes.

*/ public class BinariesArtifactSource implements ArtifactSource { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String CACHE_SUBDIR = "ondemand-plugins"; private final Path cacheBaseDirectory; private final HttpClientProvider httpClientProvider; private final BinariesSignatureVerifier signatureVerifier; private final BinariesLocalCacheManager cacheManager; private final ApplicationEventPublisher eventPublisher; private final UniqueTaskExecutor uniqueTaskExecutor; private final Map cachedArtifactPaths = new ConcurrentHashMap<>(); BinariesArtifactSource(UserPaths userPaths, HttpClientProvider httpClientProvider, ApplicationEventPublisher eventPublisher, @Qualifier("pluginDownloadExecutor") ExecutorService downloadExecutor, BinariesSignatureVerifier signatureVerifier, BinariesLocalCacheManager binariesLocalCacheManager) { this.cacheBaseDirectory = userPaths.getStorageRoot().resolve(CACHE_SUBDIR); this.httpClientProvider = httpClientProvider; this.signatureVerifier = signatureVerifier; this.cacheManager = binariesLocalCacheManager; this.eventPublisher = eventPublisher; this.uniqueTaskExecutor = new UniqueTaskExecutor(downloadExecutor); } /** * Returns all artifacts known to this source whose languages intersect {@code enabledLanguages}. * No downloads triggered. */ @Override public List listAvailableArtifacts(Set enabledLanguages) { return Arrays.stream(BinariesArtifact.values()) .filter(artifact -> artifact.getLanguages().stream().anyMatch(enabledLanguages::contains)) .map(artifact -> new AvailableArtifact(artifact.artifactKey(), Version.create(artifact.version()), false, SonarPlugin.findByKey(artifact.artifactKey()).map(p -> p).or(() -> SonarPluginDependency.findByKey(artifact.artifactKey())))) .toList(); } @Override public LoadResult load(Set artifactKeys) { var resolved = new HashMap(); for (var key : artifactKeys) { BinariesArtifact.findByKey(key).ifPresent(artifact -> { var resolvedArtifact = findCachedArtifact(artifact) .map(cached -> toActiveArtifact(artifact, cached.path())) .orElseGet(() -> scheduleDownload(artifact)); resolved.put(key, resolvedArtifact); }); } return new LoadResult(resolved); } private ResolvedArtifact scheduleDownload(BinariesArtifact artifact) { var downloadFuture = uniqueTaskExecutor.scheduleIfAbsent(artifact.artifactKey(), () -> downloadAndFireEvent(artifact)); return new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, downloadFuture); } private Optional findCachedArtifact(BinariesArtifact artifact) { var artifactKey = artifact.artifactKey(); var cached = cachedArtifactPaths.get(artifactKey); if (cached != null && Files.exists(cached)) { return Optional.of(toActiveArtifact(artifact, cached)); } var pluginPath = buildArtifactLocalPath(artifact); if (Files.exists(pluginPath)) { if (isValidCache(pluginPath, artifact)) { cachedArtifactPaths.put(artifactKey, pluginPath); return Optional.of(toActiveArtifact(artifact, pluginPath)); } LOG.warn("Invalid cached artifact {}, will re-download", artifactKey); FileUtils.deleteQuietly(pluginPath.toFile()); } return Optional.empty(); } /** * Returns {@code true} when the cached artifact at {@code pluginPath} is considered valid and * does not need to be re-downloaded. * *

For JAR artifacts the PGP signature is re-verified against the file on disk. * *

For archive artifacts (OmniSharp tar.gz distributions), {@code pluginPath} is the * extracted directory. The PGP signature was already verified against the original archive at * download time; the archive is deleted after extraction, so the signature cannot be re-checked. * A non-empty directory is used as the completion marker instead. */ private boolean isValidCache(Path pluginPath, BinariesArtifact artifact) { if (artifact.isArchive()) { try (var entries = Files.list(pluginPath)) { return entries.findFirst().isPresent(); } catch (IOException e) { LOG.warn("Could not read cached archive directory for {}", artifact.artifactKey(), e); return false; } } return signatureVerifier.verify(pluginPath, artifact); } private static ResolvedArtifact toActiveArtifact(BinariesArtifact artifact, Path artifactPath) { return new ResolvedArtifact(ArtifactState.ACTIVE, artifactPath, ArtifactOrigin.ON_DEMAND, Version.create(artifact.version()), null); } private Path downloadAndCache(BinariesArtifact artifact) throws IOException { var pluginPath = buildArtifactLocalPath(artifact); downloadAndVerify(artifact, pluginPath); cacheManager.cleanupOldVersions(cacheBaseDirectory.resolve(artifact.artifactKey()), artifact.version()); cachedArtifactPaths.put(artifact.artifactKey(), pluginPath); return pluginPath; } private void downloadAndFireEvent(BinariesArtifact artifact) { try { var path = downloadAndCache(artifact); eventPublisher.publishEvent(new PluginStatusUpdateEvent(null, createSuccessStatuses(artifact, path))); } catch (Exception e) { LOG.error("Failed to download artifact with key {}", artifact.artifactKey(), e); eventPublisher.publishEvent(new PluginStatusUpdateEvent(null, createdFailedStatuses(artifact))); } } private static List createSuccessStatuses(BinariesArtifact artifact, Path pluginPath) { if (artifact.isArchive()) { return List.of(PluginStatus.forCompanion(artifact.artifactKey(), ArtifactState.ACTIVE, ArtifactOrigin.ON_DEMAND, pluginPath, null)); } var version = Version.create(artifact.version()); return artifact.getLanguages().stream() .map(language -> PluginStatus.forLanguage(language, ArtifactState.ACTIVE, ArtifactOrigin.ON_DEMAND, version, null, pluginPath, null)) .toList(); } private static List createdFailedStatuses(BinariesArtifact artifact) { if (artifact.isArchive()) { return List.of(PluginStatus.forCompanion(artifact.artifactKey(), ArtifactState.FAILED, null, null, null)); } return artifact.getLanguages().stream() .map(PluginStatus::failed) .toList(); } public Map getOmnisharpExtraProperties() { var properties = new HashMap(); putIfCached(properties, SonarPluginDependency.OMNISHARP_MONO.getKey(), "sonar.cs.internal.omnisharpMonoLocation"); putIfCached(properties, SonarPluginDependency.OMNISHARP_NET472.getKey(), "sonar.cs.internal.omnisharpWinLocation"); putIfCached(properties, SonarPluginDependency.OMNISHARP_NET6.getKey(), "sonar.cs.internal.omnisharpNet6Location"); return properties; } private void putIfCached(Map properties, String artifactKey, String propertyKey) { var path = cachedArtifactPaths.get(artifactKey); if (path != null && Files.exists(path)) { properties.put(propertyKey, path.toString()); } } private void downloadAndVerify(BinariesArtifact artifact, Path targetPath) throws IOException { Files.createDirectories(targetPath.getParent()); var tempFile = targetPath.getParent().resolve(targetPath.getFileName() + ".tmp"); try { downloadArtifact(artifact, tempFile); if (!signatureVerifier.verify(tempFile, artifact)) { throw new IOException("Signature verification failed for " + artifact.artifactKey()); } if (artifact.isArchive()) { var tempExtractDir = targetPath.getParent().resolve(targetPath.getFileName() + ".extracting"); try { Files.createDirectories(tempExtractDir); extractTarGz(tempFile, tempExtractDir); moveAtomically(tempExtractDir, targetPath); } finally { FileUtils.deleteQuietly(tempExtractDir.toFile()); } } else { moveAtomically(tempFile, targetPath); } LOG.info("Successfully downloaded {} plugin version {}", artifact.artifactKey(), artifact.version()); } finally { Files.deleteIfExists(tempFile); } } private static void moveAtomically(Path source, Path target) throws IOException { try { Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } catch (AtomicMoveNotSupportedException e) { Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); } } private void downloadArtifact(BinariesArtifact artifact, Path destination) throws IOException { var url = String.format(artifact.urlPattern(), artifact.version()); var httpClient = httpClientProvider.getHttpClientWithoutAuth(); LOG.info("Downloading {} plugin version {} from {}", artifact.artifactKey(), artifact.version(), url); try (var response = httpClient.get(url)) { if (!response.isSuccessful()) { throw new IOException("Failed to download plugin: HTTP " + response.code()); } try (var inputStream = response.bodyAsStream()) { FileUtils.copyInputStreamToFile(inputStream, destination.toFile()); } } } private Path buildArtifactLocalPath(BinariesArtifact artifact) { var artifactKey = artifact.artifactKey(); var version = artifact.version(); var base = cacheBaseDirectory.resolve(artifactKey).resolve(version); if (artifact.isArchive()) { return base; } return base.resolve(String.format("sonar-%s-plugin-%s.jar", artifactKey, version)); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesLocalCacheManager.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Manages cleanup of old plugin versions from the cache. * Deletes version directories not modified within the last 60 days, skipping the current version. */ public class BinariesLocalCacheManager { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final long RETENTION_DAYS = 60; /** * Cleans up old plugin versions from the cache directory. * * @param cacheDirectory the base cache directory (e.g., {storageRoot}/cache/ondemand-plugins/cpp) * @param currentVersion the current version to keep (not deleted) */ void cleanupOldVersions(Path cacheDirectory, String currentVersion) { if (!Files.isDirectory(cacheDirectory)) { return; } var cutoffTime = Instant.now().minus(RETENTION_DAYS, ChronoUnit.DAYS); try (var stream = Files.list(cacheDirectory)) { stream.filter(Files::isDirectory) .filter(versionDir -> !versionDir.getFileName().toString().equals(currentVersion)) .filter(versionDir -> isOlderThan(versionDir, cutoffTime)) .forEach(BinariesLocalCacheManager::deleteVersionDirectory); } catch (IOException e) { LOG.debug("Error cleaning up old plugin versions", e); } } private static boolean isOlderThan(Path directory, Instant cutoffTime) { try { var lastModified = Files.getLastModifiedTime(directory).toInstant(); return lastModified.isBefore(cutoffTime); } catch (IOException e) { LOG.debug("Failed to read last-modified time for plugin cache directory: {}", directory, e); return false; } } private static void deleteVersionDirectory(Path directory) { try { FileUtils.deleteDirectory(directory.toFile()); LOG.debug("Deleted old plugin version: {}", directory.getFileName()); } catch (Exception e) { LOG.debug("Failed to delete old version directory: {}", directory, e); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesSignatureVerifier.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Verifies the PGP signature of downloaded artifacts using the SonarSource public key. */ public class BinariesSignatureVerifier { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String SONAR_PUBLIC_KEY = "ondemand/sonarsource-public.key"; private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); boolean verify(Path artifactFile, BinariesArtifact artifact) { return verify(artifactFile, artifact.signatureResourcePath()); } boolean verify(Path artifactFile, String signatureResourcePath) { var keyRing = loadPublicKeyRing(); if (keyRing == null) { return false; } var isValid = verifyPgpSignature(artifactFile, signatureResourcePath, keyRing); if (isValid) { LOG.debug("Artifact file signature verified successfully"); } return isValid; } private PGPPublicKeyRingCollection loadPublicKeyRing() { try (var keyStream = getClass().getClassLoader().getResourceAsStream(SONAR_PUBLIC_KEY)) { if (keyStream == null) { throw new FileNotFoundException("PGP key not found in resources: " + SONAR_PUBLIC_KEY); } var decoder = PGPUtil.getDecoderStream(new BufferedInputStream(keyStream)); return new PGPPublicKeyRingCollection(decoder, new JcaKeyFingerprintCalculator()); } catch (IOException | PGPException e) { LOG.error("Error loading public key ring", e); return null; } } private InputStream loadBundledSignature(String signatureResourcePath) { return getClass().getClassLoader().getResourceAsStream(signatureResourcePath); } private boolean verifyPgpSignature(Path dataFile, String signatureResourcePath, PGPPublicKeyRingCollection keyRing) { try (var signatureStream = loadBundledSignature(signatureResourcePath)) { if (signatureStream == null) { LOG.error("Could not find bundled signature at resource path: {}", signatureResourcePath); return false; } try (var decoderStream = PGPUtil.getDecoderStream(new BufferedInputStream(signatureStream))) { var pgpFact = new PGPObjectFactory(decoderStream, new JcaKeyFingerprintCalculator()); // Handle both compressed and uncompressed signature formats var signatureList = extractSignatureList(pgpFact); if (signatureList == null || signatureList.isEmpty()) { LOG.error("No signatures found in signature file"); return false; } var signature = signatureList.get(0); var publicKey = keyRing.getPublicKey(signature.getKeyID()); if (publicKey == null) { LOG.error("Public key not found for signature keyID={}", signature.getKeyID()); return false; } signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(BOUNCY_CASTLE_PROVIDER), publicKey); try (var dataIn = new FileInputStream(dataFile.toFile())) { var buffer = new byte[8192]; int bytesRead; while ((bytesRead = dataIn.read(buffer)) != -1) { signature.update(buffer, 0, bytesRead); } } return signature.verify(); } } catch (IOException | PGPException e) { LOG.error("Error verifying PGP signature", e); return false; } } private static PGPSignatureList extractSignatureList(PGPObjectFactory pgpFact) { try { var obj = pgpFact.nextObject(); if (obj instanceof PGPCompressedData compressedData) { var innerFactory = new PGPObjectFactory(compressedData.getDataStream(), new JcaKeyFingerprintCalculator()); var innerObj = innerFactory.nextObject(); if (innerObj instanceof PGPSignatureList signatureList) { return signatureList; } } else if (obj instanceof PGPSignatureList signatureList) { return signatureList; } } catch (IOException | PGPException e) { LOG.error("Error extracting signature list", e); } return null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/binaries/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.source.binaries; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/embedded/EmbeddedPluginSource.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.embedded; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.PluginJarUtils; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginManifest; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; /** * Artifact source backed by JARs physically bundled (embedded) in the IDE client's distribution. * Does not do any filtering, trusts what the client provides. No downloads are ever triggered. * *

Use {@link #forStandalone(InitializeParams)} or {@link #forConnected(InitializeParams)} to * obtain an instance scoped to the appropriate mode.

*/ public class EmbeddedPluginSource implements ArtifactSource { private final Map embeddedPathsByKey; private EmbeddedPluginSource(Map embeddedPathsByKey) { this.embeddedPathsByKey = embeddedPathsByKey; } /** * Returns a source backed by the standalone embedded plugin paths from {@code params}. */ public static EmbeddedPluginSource forStandalone(InitializeParams params) { return new EmbeddedPluginSource(buildPluginKeyToPathMap(params.getEmbeddedPluginPaths())); } /** * Returns a source backed by the connected-mode embedded plugin paths from {@code params}. */ public static EmbeddedPluginSource forConnected(InitializeParams params) { return new EmbeddedPluginSource(params.getConnectedModeEmbeddedPluginPathsByKey()); } /** * Returns all artifacts physically embedded in the IDE client. No downloads are ever triggered. * We ignore the enabledLanguages parameter for this source. We trust the clients to provide sensible embedded artifacts. */ @Override public List listAvailableArtifacts(Set enabledLanguages) { var result = new ArrayList(); for (var entry : embeddedPathsByKey.entrySet()) { result.add(toAvailableArtifact(entry.getKey(), entry.getValue())); } return result; } @Override public LoadResult load(Set artifactKeys) { var resolved = new HashMap(); for (var key : artifactKeys) { var path = embeddedPathsByKey.get(key); if (path != null) { resolved.put(key, new ResolvedArtifact(ArtifactState.ACTIVE, path, ArtifactOrigin.EMBEDDED, PluginJarUtils.readVersion(path), null)); } } return new LoadResult(resolved); } private static AvailableArtifact toAvailableArtifact(String key, Path path) { var sonarPlugin = SonarPlugin.findByKey(key); return new AvailableArtifact(key, PluginJarUtils.readVersion(path), SonarPlugin.isEnterpriseVariant(key), sonarPlugin); } private static Map buildPluginKeyToPathMap(Set embeddedPaths) { return embeddedPaths.stream() .collect(Collectors.toMap( p -> SonarPluginManifest.fromJar(p).getKey(), Function.identity(), (existing, duplicate) -> { throw new IllegalArgumentException("Multiple embedded plugins found with the same key for paths: " + existing + " and " + duplicate); })); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/embedded/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.source.embedded; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.source; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginDownloader.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.PluginStatusUpdateEvent; import org.sonarsource.sonarlint.core.plugin.PluginJarUtils; import org.sonarsource.sonarlint.core.plugin.PluginStatus; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.UniqueTaskExecutor; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; /** * Handles the background downloading of server plugins (both language and companion plugins). * Manages concurrent requests deduplication and publishes plugin status updates upon completion. */ public class ServerPluginDownloader { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StorageService storageService; private final SonarQubeClientManager sonarQubeClientManager; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final ApplicationEventPublisher eventPublisher; private final UniqueTaskExecutor uniqueTaskExecutor; public ServerPluginDownloader(StorageService storageService, SonarQubeClientManager sonarQubeClientManager, ConnectionConfigurationRepository connectionConfigurationRepository, ApplicationEventPublisher eventPublisher, ExecutorService downloadExecutor) { this.storageService = storageService; this.sonarQubeClientManager = sonarQubeClientManager; this.connectionConfigurationRepository = connectionConfigurationRepository; this.eventPublisher = eventPublisher; this.uniqueTaskExecutor = new UniqueTaskExecutor(downloadExecutor); } public CompletableFuture schedulePluginDownload(String connectionId, ServerPlugin serverPlugin) { var sonarPlugin = SonarPlugin.findByKey(serverPlugin.getKey()); return sonarPlugin.isPresent() ? scheduleSonarPluginDownload(connectionId, serverPlugin, sonarPlugin.get()) : scheduleUnknownPluginDownload(connectionId, serverPlugin); } private CompletableFuture scheduleSonarPluginDownload(String connectionId, ServerPlugin serverPlugin, SonarPlugin sonarPlugin) { var progressKey = connectionId + ":" + serverPlugin.getKey(); return uniqueTaskExecutor.scheduleIfAbsent(progressKey, () -> asyncDownload(connectionId, serverPlugin, sonarPlugin)); } private CompletableFuture scheduleUnknownPluginDownload(String connectionId, ServerPlugin plugin) { var progressKey = connectionId + ":" + plugin.getKey(); return uniqueTaskExecutor.scheduleIfAbsent(progressKey, () -> asyncUnknownPluginDownload(connectionId, plugin)); } private void asyncDownload(String connectionId, ServerPlugin serverPlugin, SonarPlugin sonarPlugin) { try { downloadPluginAndFireEvent(connectionId, serverPlugin, sonarPlugin); } catch (Exception e) { LOG.error("Failed to download plugin '{}' for connection '{}'", serverPlugin.getKey(), connectionId, e); fireFailedEvent(connectionId, sonarPlugin); } } private void asyncUnknownPluginDownload(String connectionId, ServerPlugin plugin) { try { downloadUnknownPluginAndFireEvent(connectionId, plugin); } catch (Exception e) { LOG.error("Failed to download companion plugin '{}' for connection '{}'", plugin.getKey(), connectionId, e); eventPublisher.publishEvent(new PluginStatusUpdateEvent(connectionId, List.of(PluginStatus.forCompanion(plugin.getKey(), ArtifactState.FAILED, null, null, null)))); } } private void downloadPluginAndFireEvent(String connectionId, ServerPlugin serverPlugin, SonarPlugin sonarPlugin) { var state = downloadPluginSync(connectionId, serverPlugin); if (state == ArtifactState.SYNCED) { var pluginKey = serverPlugin.getKey(); var storedPath = storageService.connection(connectionId).plugins().getStoredPluginPathsByKey().get(pluginKey); var source = sourceFor(connectionId); var version = storedPath != null ? PluginJarUtils.readVersion(storedPath) : null; var statuses = sonarPlugin.getLanguages().stream() .map(l -> PluginStatus.forLanguage(l, ArtifactState.SYNCED, source, version, null, storedPath, null)) .toList(); eventPublisher.publishEvent(new PluginStatusUpdateEvent(connectionId, statuses)); } else { fireFailedEvent(connectionId, sonarPlugin); } } private void downloadUnknownPluginAndFireEvent(String connectionId, ServerPlugin plugin) { var state = downloadPluginSync(connectionId, plugin); var storedPath = state == ArtifactState.SYNCED ? storageService.connection(connectionId).plugins().getStoredPluginPathsByKey().get(plugin.getKey()) : null; var source = sourceFor(connectionId); eventPublisher.publishEvent(new PluginStatusUpdateEvent(connectionId, List.of(PluginStatus.forCompanion(plugin.getKey(), state, source, storedPath, null)))); } ArtifactState downloadPluginSync(String connectionId, ServerPlugin serverPlugin) { var pluginKey = serverPlugin.getKey(); LOG.info("[SYNC] Downloading plugin '{}'", serverPlugin.getFilename()); try { var cancelMonitor = new SonarLintCancelMonitor(); sonarQubeClientManager.withActiveClient(connectionId, api -> api.plugins().getPlugin(pluginKey, binary -> storageService.connection(connectionId).plugins().store(serverPlugin, binary), cancelMonitor)); return ArtifactState.SYNCED; } catch (Exception e) { LOG.error("Failed to download plugin '{}' for connection '{}'", pluginKey, connectionId, e); return ArtifactState.FAILED; } } private void fireFailedEvent(String connectionId, SonarPlugin sonarPlugin) { var statuses = sonarPlugin.getLanguages().stream() .map(PluginStatus::failed) .toList(); eventPublisher.publishEvent(new PluginStatusUpdateEvent(connectionId, statuses)); } public ArtifactOrigin sourceFor(String connectionId) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); var isSonarQubeCloud = connection != null && connection.getKind() == ConnectionKind.SONARCLOUD; return isSonarQubeCloud ? ArtifactOrigin.SONARQUBE_CLOUD : ArtifactOrigin.SONARQUBE_SERVER; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginSource.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.EnterpriseReplacement; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.PluginJarUtils; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin; import org.sonarsource.sonarlint.core.storage.StorageService; /** * Artifact source backed by a specific SonarQube Server or SonarQube Cloud connection. * *

One instance is created per connection. The connection identifier is fixed at construction * time; no connection parameter is passed at call time.

* *

{@link #listAvailableArtifacts(Set)} queries the server for available plugins and filters * them by enabled languages and SonarLint compatibility. Language plugins are included if any of * their languages is enabled. Companion/unknown plugins are included if they are marked * {@code sonarLintSupported} by the server.

* *

The {@link AvailableArtifact#isEnterprise()} flag is set to {@code true} when the server * is serving the enterprise edition of a plugin on this connection: *

    *
  • Different-key enterprise variants ({@code csharpenterprise}, {@code vbnetenterprise}): * always enterprise.
  • *
  • Same-key enterprise plugins (GO, IAC, TEXT): enterprise when the connection qualifies * (SonarQube Cloud, or SonarQube Server ≥ the minimum version from * {@link EnterpriseReplacement}).
  • *
* *

{@link #load} resolves a plugin: it returns the stored artifact when already on disk with a * matching hash, or schedules a background download (returning * {@link ArtifactState#DOWNLOADING}). It does not apply the skip-list check — that is * the responsibility of the loading strategy that owns this source.

*/ public class ServerPluginSource implements ArtifactSource { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String PLUGIN_FETCH_ERROR = "Could not fetch server plugin list for connection '{}'"; private final String connectionId; private final StorageService storageService; private final ServerPluginsCache serverPluginsCache; private final ServerPluginDownloader downloader; public ServerPluginSource(String connectionId, StorageService storageService, ServerPluginsCache serverPluginsCache, ServerPluginDownloader downloader) { this.connectionId = connectionId; this.storageService = storageService; this.serverPluginsCache = serverPluginsCache; this.downloader = downloader; } /** * Returns all server plugins that are eligible: *
    *
  • if a plugin is known (see {@link SonarPlugin}), at least one of its languages should be currently enabled *
  • if it is unknown, it needs to be SonarLint-Supported *
*/ @Override public List listAvailableArtifacts(Set enabledLanguages) { return fetchServerPluginsSafely().stream() .filter(plugin -> isEligible(plugin, enabledLanguages)) .map(plugin -> new AvailableArtifact(plugin.getKey(), null, isEnterprisePlugin(plugin.getKey()), SonarPlugin.findByKey(plugin.getKey()))) .toList(); } private static boolean isEligible(ServerPlugin plugin, Set enabledLanguages) { return SonarPlugin.findByKey(plugin.getKey()) .map(sonarPlugin -> { var languages = sonarPlugin.getLanguages(); return !languages.isEmpty() && languages.stream().anyMatch(lang -> lang.shouldSyncInConnectedMode() && enabledLanguages.contains(lang)); }) .orElseGet(plugin::isSonarLintSupported); } /** * Returns {@code true} if the given plugin key is served as its enterprise edition on this * connection. * *

Different-key enterprise variants ({@code csharpenterprise}, {@code vbnetenterprise}) are * always enterprise. Same-key enterprise plugins (GO, IAC, TEXT) are enterprise when the * connection is SonarQube Cloud, or when the stored server version meets the minimum.

*/ private boolean isEnterprisePlugin(String key) { if (SonarPlugin.isEnterpriseVariant(key)) { return true; } return SonarPlugin.findByKey(key) .flatMap(SonarPlugin::getEnterpriseReplacement) .map(this::hasEnterpriseReplacement) .orElse(false); } private boolean hasEnterpriseReplacement(EnterpriseReplacement replacement) { var source = downloader.sourceFor(connectionId); if (source == ArtifactOrigin.SONARQUBE_CLOUD) { return replacement.onSonarQubeCloud(); } var replacementStartingInSonarQubeServerVersion = replacement.startingSonarQubeServerVersion(); return replacementStartingInSonarQubeServerVersion != null && storageService.connection(connectionId).serverInfo().read() .map(info -> info.version().compareTo(replacementStartingInSonarQubeServerVersion) >= 0) .orElse(false); } @Override public LoadResult load(Set artifactKeys) { var storedPlugins = loadStoredPlugins(); var resolved = new HashMap(); var serverAccessible = false; List expectedServerPlugins = List.of(); try { var serverPluginsByKey = serverPluginsCache.getPlugins(connectionId).orElse(List.of()) .stream().collect(Collectors.toMap(ServerPlugin::getKey, Function.identity())); serverAccessible = true; for (var key : artifactKeys) { var serverPlugin = serverPluginsByKey.get(key); if (serverPlugin != null) { resolved.put(key, resolveFromStorageOrSchedule(serverPlugin, storedPlugins, key)); } else { findStoredPlugin(key, storedPlugins).map(s -> toResolvedArtifact(s.getJarPath())) .ifPresent(r -> resolved.put(key, r)); } } expectedServerPlugins = artifactKeys.stream() .filter(serverPluginsByKey::containsKey) .map(key -> { var serverPlugin = serverPluginsByKey.get(key); // If the stored file already has the correct hash but a different filename (e.g. in tests), // use the stored filename so cleanUpUnknownPlugins does not delete it. return findStoredPlugin(key, storedPlugins) .filter(stored -> stored.hasSameHash(serverPlugin)) .map(stored -> new ServerPlugin(key, serverPlugin.getHash(), stored.getJarPath().getFileName().toString(), serverPlugin.isSonarLintSupported())) .orElse(serverPlugin); }) .toList(); } catch (Exception e) { LOG.debug(PLUGIN_FETCH_ERROR, connectionId); for (var key : artifactKeys) { findStoredPlugin(key, storedPlugins).map(s -> toResolvedArtifact(s.getJarPath())) .ifPresent(r -> resolved.put(key, r)); } } if (serverAccessible) { storageService.connection(connectionId).plugins().cleanUpUnknownPlugins(expectedServerPlugins); } return new LoadResult(resolved); } private ResolvedArtifact resolveFromStorageOrSchedule(ServerPlugin serverPlugin, Map storedPlugins, String pluginKey) { var stored = findStoredPlugin(pluginKey, storedPlugins); if (stored.isPresent() && stored.get().hasSameHash(serverPlugin)) { LOG.debug("[SYNC] Code analyzer '{}' is up-to-date. Skip downloading it.", pluginKey); return toResolvedArtifact(stored.get().getJarPath()); } var downloadFuture = downloader.schedulePluginDownload(connectionId, serverPlugin); return new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, downloadFuture); } private static Optional findStoredPlugin(String pluginKey, Map storedPlugins) { return Optional.ofNullable(storedPlugins.get(pluginKey)) .filter(plugin -> Files.exists(plugin.getJarPath())); } private ResolvedArtifact toResolvedArtifact(Path pluginPath) { return new ResolvedArtifact(ArtifactState.SYNCED, pluginPath, downloader.sourceFor(connectionId), PluginJarUtils.readVersion(pluginPath), null); } private Map loadStoredPlugins() { try { return storageService.connection(connectionId).plugins().getStoredPluginsByKey(); } catch (Exception e) { return Collections.emptyMap(); } } private List fetchServerPluginsSafely() { try { return serverPluginsCache.getPlugins(connectionId).orElse(List.of()); } catch (Exception e) { LOG.debug(PLUGIN_FETCH_ERROR, connectionId); return storedPluginsAsServerPlugins(); } } private List storedPluginsAsServerPlugins() { return loadStoredPlugins().values().stream() .filter(s -> Files.exists(s.getJarPath())) .map(s -> new ServerPlugin(s.getKey(), s.getHash(), s.getJarPath().getFileName().toString(), true)) .toList(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginsCache.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.springframework.context.event.EventListener; public class ServerPluginsCache { private final SonarQubeClientManager sonarQubeClientManager; private final Cache>> cache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.HOURS) .build(); public ServerPluginsCache(SonarQubeClientManager sonarQubeClientManager) { this.sonarQubeClientManager = sonarQubeClientManager; } public Optional> getPlugins(String connectionId) { try { return cache.get(connectionId, () -> fetch(connectionId)); } catch (ExecutionException e) { throw new IllegalStateException(e.getCause()); } } private Optional> fetch(String connectionId) { return sonarQubeClientManager.withActiveClientAndReturn(connectionId, api -> api.plugins().getInstalled(new SonarLintCancelMonitor())); } @EventListener public void connectionRemoved(ConnectionConfigurationRemovedEvent event) { cache.invalidate(event.removedConnectionId()); } @EventListener public void connectionUpdated(ConnectionConfigurationUpdatedEvent event) { cache.invalidate(event.updatedConnectionId()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/plugin/source/server/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.source.server; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/progress/ClientAwareProgressMonitor.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.progress; import java.util.UUID; import org.jetbrains.annotations.Nullable; import org.sonarsource.sonarlint.core.commons.progress.ProgressMonitor; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ProgressEndNotification; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ProgressUpdateNotification; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ReportProgressParams; public class ClientAwareProgressMonitor implements ProgressMonitor { private final SonarLintRpcClient client; private final UUID taskId; private final SonarLintCancelMonitor cancelMonitor; public ClientAwareProgressMonitor(SonarLintRpcClient client, UUID taskId, SonarLintCancelMonitor cancelMonitor) { this.client = client; this.taskId = taskId; this.cancelMonitor = cancelMonitor; } @Override public void notifyProgress(@Nullable String message, @Nullable Integer percentage) { client.reportProgress(new ReportProgressParams(taskId.toString(), new ProgressUpdateNotification(message, percentage))); } @Override public boolean isCanceled() { return cancelMonitor.isCanceled(); } @Override public void cancel() { cancelMonitor.cancel(); } @Override public void complete() { client.reportProgress(new ReportProgressParams(taskId.toString(), new ProgressEndNotification())); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/progress/ClientAwareTaskManager.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.progress; import java.util.UUID; import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.progress.CanceledException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ProgressMonitor; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.StartProgressParams; public class ClientAwareTaskManager extends TaskManager { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; public ClientAwareTaskManager(SonarLintRpcClient client) { this.client = client; } @Override protected void startProgress(@Nullable String configurationScopeId, UUID taskId, String title, @Nullable String message, boolean indeterminate, boolean cancellable, SonarLintCancelMonitor cancelMonitor) { try { client.startProgress(new StartProgressParams(taskId.toString(), configurationScopeId, title, message, indeterminate, cancellable)).get(); } catch (InterruptedException e) { LOG.error("The progress report for the '" + title + "' was interrupted", e); Thread.currentThread().interrupt(); throw new CanceledException(); } catch (ExecutionException e) { LOG.error("The client was unable to start progress, cause:", e); super.startProgress(configurationScopeId, taskId, title, message, indeterminate, cancellable, cancelMonitor); } } @Override protected ProgressMonitor createProgress(UUID taskId, SonarLintCancelMonitor cancelMonitor) { return new ClientAwareProgressMonitor(client, taskId, cancelMonitor); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/progress/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.progress; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/LanguagePromotionService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.analysis.AnalysisFinishedEvent; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.promotion.PromoteExtraEnabledLanguagesInConnectedModeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.springframework.context.event.EventListener; public class LanguagePromotionService { private final ConfigurationRepository configurationRepository; private final Set extraEnabledLanguagesInConnectedMode; private final SonarLintRpcClient client; public LanguagePromotionService(ConfigurationRepository configurationRepository, InitializeParams initializeParams, SonarLintRpcClient client) { this.configurationRepository = configurationRepository; this.extraEnabledLanguagesInConnectedMode = initializeParams.getExtraEnabledLanguagesInConnectedMode(); this.client = client; } @EventListener public void onAnalysisFinished(AnalysisFinishedEvent event) { var configurationScopeId = event.getConfigurationScopeId(); if (isStandalone(configurationScopeId)) { var languagesToPromote = getLanguagesToPromote(event.getDetectedLanguages()); if (!languagesToPromote.isEmpty()) { client.promoteExtraEnabledLanguagesInConnectedMode(new PromoteExtraEnabledLanguagesInConnectedModeParams(configurationScopeId, languagesToPromote)); } } } private boolean isStandalone(String configurationScopeId) { return configurationRepository.getEffectiveBinding(configurationScopeId).isEmpty(); } private Set getLanguagesToPromote(Set detectedLanguages) { var languagesToPromote = detectedLanguages.stream().map(sonarLanguage -> Language.valueOf(sonarLanguage.name())).collect(Collectors.toCollection(HashSet::new)); languagesToPromote.retainAll(extraEnabledLanguagesInConnectedMode); return languagesToPromote; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/PromotionSpringConfig.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion; import java.nio.file.Path; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.promotion.campaign.CampaignService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import({ LanguagePromotionService.class, CampaignService.class }) public class PromotionSpringConfig { @Bean Path campaignsPath(UserPaths userPaths) { return userPaths.getHomeIdeSpecificDir("campaigns").resolve("campaigns"); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/CampaignConstants.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign; public class CampaignConstants { public static final String FEEDBACK_2026_01_CAMPAIGN = "feedback_2026_01"; private static final String JETBRAINS_MARKETPLACE = "https://plugins.jetbrains.com/plugin/7973-sonarqube-for-ide/reviews"; private static final String VS_MARKETPLACE = "https://marketplace.visualstudio.com/items?itemName=SonarSource.SonarLintforVisualStudio2022&ssr=false#review-details"; private static final String VSCODE_MARKETPLACE = "https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarlint-vscode&ssr=false#review-details"; private static final String OPEN_VSX = "https://open-vsx.org/extension/SonarSource/sonarlint-vscode/reviews"; private static final String INTELLIJ_GOOGLE_FORM = "https://forms.gle/kDyQ7sDyBfpPEBsy6"; private static final String VISUAL_STUDIO_GOOGLE_FORM = "https://forms.gle/LjKGKWECDdJw1PmU7"; private static final String VS_CODE_GOOGLE_FORM = "https://forms.gle/TncKAVK4EWM7z4RV6"; private CampaignConstants() { } static String urlToOpen(FeedbackNotificationActionItem response, String productKey) { return switch (response) { case LOVE_IT -> switch (productKey) { case "idea" -> JETBRAINS_MARKETPLACE; case "visualstudio" -> VS_MARKETPLACE; case "vscode" -> VSCODE_MARKETPLACE; case "windsurf", "cursor", "kiro" -> OPEN_VSX; default -> null; }; case SHARE_FEEDBACK -> switch (productKey) { case "idea" -> INTELLIJ_GOOGLE_FORM; case "visualstudio" -> VISUAL_STUDIO_GOOGLE_FORM; case "vscode", "windsurf", "cursor", "kiro" -> VS_CODE_GOOGLE_FORM; default -> null; }; default -> null; }; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/CampaignResolvedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign; public record CampaignResolvedEvent(String campaignName, String resolution) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/CampaignService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.nio.file.Path; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.Period; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import javax.annotation.PostConstruct; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.math.NumberUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.storage.local.FileStorageManager; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.promotion.campaign.storage.CampaignsLocalStorage; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.OpenUrlInBrowserParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageActionItem; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestResponse; import org.sonarsource.sonarlint.core.telemetry.InternalDebug; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import static java.util.concurrent.TimeUnit.SECONDS; import static org.sonarsource.sonarlint.core.promotion.campaign.FeedbackNotificationActionItem.LOVE_IT; import static org.sonarsource.sonarlint.core.promotion.campaign.FeedbackNotificationActionItem.MAYBE_LATER; import static org.sonarsource.sonarlint.core.promotion.campaign.FeedbackNotificationActionItem.SHARE_FEEDBACK; import static org.sonarsource.sonarlint.core.promotion.campaign.storage.CampaignsLocalStorage.Campaign; public class CampaignService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Set RESPONSES_TO_OPEN_URL = EnumSet.of(LOVE_IT, SHARE_FEEDBACK); private static final Map POSTPONE_PERIODS = Map.of( MAYBE_LATER.name(), Period.ofWeeks(1), "IGNORE", Period.ofMonths(6) ); private static final String SIX_MINUTES_OF_SECONDS = "360"; private static final int TWO_WEEKS = 14; private static final String CLOSED_BY_USER_ACTION_KEY = "CLOSED"; private final String productKey; private final SonarLintRpcClient client; private final TelemetryService telemetryService; private final FileStorageManager fileStorageManager; private final ScheduledExecutorService scheduledExecutor; private final ApplicationEventPublisher eventPublisher; private final boolean isEnabled; public CampaignService(@Qualifier("campaignsPath") Path campaignsPath, SonarLintRpcClient client, InitializeParams initializeParams, TelemetryService telemetryService, ApplicationEventPublisher eventPublisher) { this.productKey = initializeParams.getTelemetryConstantAttributes().getProductKey(); this.client = client; this.telemetryService = telemetryService; this.fileStorageManager = new FileStorageManager<>(campaignsPath, CampaignsLocalStorage::new, CampaignsLocalStorage.class); this.eventPublisher = eventPublisher; this.scheduledExecutor = FailSafeExecutors.newSingleThreadScheduledExecutor("SonarLint Telemetry"); this.isEnabled = initializeParams.getBackendCapabilities().contains(BackendCapability.PROMOTIONAL_CAMPAIGNS); } @PostConstruct public void checkCampaigns() { if (isEnabled && shouldShowFeedbackNotification()) { var initialDelayProperty = System.getProperty("sonarlint.internal.promotion.initialDelay", SIX_MINUTES_OF_SECONDS); var initialDelay = NumberUtils.toInt(initialDelayProperty, 360); scheduledExecutor.schedule(this::showFeedbackMessage, initialDelay, SECONDS); } } private boolean shouldShowFeedbackNotification() { var campaigns = fileStorageManager.getStorage().campaigns(); var feedbackCampaign = campaigns.get(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN); if (feedbackCampaign != null) { var lastResponse = feedbackCampaign.lastUserResponse(); return isPostponeResponse(lastResponse) && postponeTimePassed(lastResponse, feedbackCampaign); } else { return isInstalledLongEnough(); } } private static boolean isPostponeResponse(String lastResponse) { return POSTPONE_PERIODS.containsKey(lastResponse); } private boolean isInstalledLongEnough() { return OffsetDateTime.now().minusDays(TWO_WEEKS).isAfter(telemetryService.installTime()); } private static boolean postponeTimePassed(String lastResponse, Campaign feedbackCampaign) { var postpone = POSTPONE_PERIODS.get(lastResponse); var lastShown = feedbackCampaign.lastNotificationShownOn(); return lastShown.plus(postpone).isBefore(LocalDate.now()); } private void showFeedbackMessage() { // Check if notification was already shown today before actually showing it // This prevents duplicate notifications if multiple instances scheduled it var shouldShow = tryMarkAsShownToday(); if (shouldShow) { eventPublisher.publishEvent(new CampaignShownEvent(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN)); var userChoice = client.showMessageRequest(new ShowMessageRequestParams( MessageType.INFO, "Enjoying SonarQube for IDE? We'd love to hear what you think.", getFeedbackNotificationActions() )); userChoice.thenAccept(this::handleFeedbackResponse); } } private boolean tryMarkAsShownToday() { var shouldShow = new AtomicBoolean(false); fileStorageManager.tryUpdateAtomically(storage -> { var campaigns = storage.campaigns(); var existingCampaign = campaigns.get(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN); if (existingCampaign == null || !wasShownRecently(existingCampaign.lastNotificationShownOn())) { campaigns.put(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN, new Campaign(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN, LocalDate.now(), "IGNORE")); shouldShow.set(true); } }); return shouldShow.get(); } private static boolean wasShownRecently(LocalDate lastShown) { var today = LocalDate.now(); var yesterday = today.minusDays(1); // Consider "recently shown" if shown today or yesterday, this prevents midnight edge case where notification fires just after midnight return lastShown.equals(today) || lastShown.equals(yesterday); } private static List getFeedbackNotificationActions() { return Stream.of(FeedbackNotificationActionItem.values()) .map(FeedbackNotificationActionItem::toMessageActionItem) .toList(); } private void handleFeedbackResponse(ShowMessageRequestResponse response) { Optional.of(response) .map(r -> r.isClosedByUser() ? CLOSED_BY_USER_ACTION_KEY : r.getSelectedKey()) .ifPresent(this::handleFeedbackResponse); } private void handleFeedbackResponse(String responseOption) { fileStorageManager.tryUpdateAtomically(storage -> storage.campaigns().put( CampaignConstants.FEEDBACK_2026_01_CAMPAIGN, new Campaign(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN, LocalDate.now(), responseOption))); eventPublisher.publishEvent(new CampaignResolvedEvent(CampaignConstants.FEEDBACK_2026_01_CAMPAIGN, responseOption)); var response = EnumUtils.getEnum(FeedbackNotificationActionItem.class, responseOption); if (RESPONSES_TO_OPEN_URL.contains(response)) { var url = CampaignConstants.urlToOpen(response, productKey); if (url != null) { client.openUrlInBrowser(new OpenUrlInBrowserParams(url)); } else { redirectToCommunityIfNoLinkFound(); } } } private void redirectToCommunityIfNoLinkFound() { var showMessageRequestParams = new ShowMessageRequestParams(MessageType.INFO, "Could not find feedback link for " + productKey + ". Please consider sharing your feedback directly on our community forum", List.of(new MessageActionItem("OPEN_COMMUNITY", "Open Community Forum", true))); client.showMessageRequest(showMessageRequestParams) .thenAccept(response -> { if (response.getSelectedKey() != null && response.getSelectedKey().equals("OPEN_COMMUNITY")) { client.openUrlInBrowser(new OpenUrlInBrowserParams("https://community.sonarsource.com/c/sl/11")); } }); } @PreDestroy public void close() { if ((!MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, 1, TimeUnit.SECONDS)) && (InternalDebug.isEnabled())) { LOG.error("Failed to stop Campaign Service executor"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/CampaignShownEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign; public record CampaignShownEvent(String campaignName) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/FeedbackNotificationActionItem.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageActionItem; public enum FeedbackNotificationActionItem { LOVE_IT("Love it!", true), SHARE_FEEDBACK("Share Feedback", true), MAYBE_LATER("Maybe Later", false); private final String message; private final boolean isPrimaryAction; FeedbackNotificationActionItem(String message, boolean isPrimaryAction) { this.message = message; this.isPrimaryAction = isPrimaryAction; } public MessageActionItem toMessageActionItem() { return new MessageActionItem(name(), message, isPrimaryAction); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.promotion.campaign; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/storage/CampaignsLocalStorage.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.promotion.campaign.storage; import java.time.LocalDate; import java.util.HashMap; import java.util.Map; import org.sonarsource.sonarlint.core.commons.storage.local.LocalStorage; public record CampaignsLocalStorage(Map campaigns) implements LocalStorage { public CampaignsLocalStorage() { this(new HashMap<>()); } public record Campaign(String campaignName, LocalDate lastNotificationShownOn, String lastUserResponse) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/campaign/storage/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.promotion.campaign.storage; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/promotion/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.promotion; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixFeature.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.remediation.aicodefix; import org.sonarsource.sonarlint.core.repository.reporting.RaisedIssue; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixSettings; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; public record AiCodeFixFeature(AiCodeFixSettings settings) { public boolean isFixable(TrackedIssue issue) { return settings.supportedRules().contains(issue.getRuleKey()) && issue.getTextRangeWithHash() != null; } public boolean isFixable(RaisedIssue issue) { return settings.supportedRules().contains(issue.issueDto().getRuleKey()) && issue.issueDto().getTextRange() != null; } public boolean isFixable(ServerTaintIssue serverTaintIssue) { return settings.supportedRules().contains(serverTaintIssue.getRuleKey()) && serverTaintIssue.getTextRange() != null; } public boolean isFixable(TaintVulnerabilityDto taintDto) { return settings.supportedRules().contains(taintDto.getRuleKey()) && taintDto.getTextRange() != null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.remediation.aicodefix; import com.google.common.collect.Sets; import java.util.Optional; import java.util.UUID; import javax.annotation.Nullable; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.FixSuggestionReceivedEvent; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.reporting.PreviouslyRaisedFindingsRepository; import org.sonarsource.sonarlint.core.repository.reporting.RaisedIssue; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixChangeDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.TooManyRequestsException; import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionRequestBodyDto; import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionResponseBodyDto; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFix; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixFeatureEnablement; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixSettings; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import org.springframework.context.ApplicationEventPublisher; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONNECTION_NOT_FOUND; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.FILE_NOT_FOUND; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.ISSUE_NOT_FOUND; import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.TOO_MANY_REQUESTS; public class AiCodeFixService { private final ConnectionConfigurationRepository connectionRepository; private final ConfigurationRepository configurationRepository; private final SonarQubeClientManager sonarQubeClientManager; private final PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository; private final ClientFileSystemService clientFileSystemService; private final ApplicationEventPublisher eventPublisher; private final TaintVulnerabilityTrackingService taintVulnerabilityTrackingService; private final AiCodeFixRepository aiCodeFixRepository; public AiCodeFixService(ConnectionConfigurationRepository connectionRepository, ConfigurationRepository configurationRepository, SonarQubeClientManager sonarQubeClientManager, PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, ClientFileSystemService clientFileSystemService, ApplicationEventPublisher eventPublisher, TaintVulnerabilityTrackingService taintVulnerabilityTrackingService, AiCodeFixRepository aiCodeFixRepository) { this.connectionRepository = connectionRepository; this.configurationRepository = configurationRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.previouslyRaisedFindingsRepository = previouslyRaisedFindingsRepository; this.clientFileSystemService = clientFileSystemService; this.eventPublisher = eventPublisher; this.taintVulnerabilityTrackingService = taintVulnerabilityTrackingService; this.aiCodeFixRepository = aiCodeFixRepository; } public static AiCodeFixSettings aiCodeFixMapping(AiCodeFix entity) { return new AiCodeFixSettings( Sets.newHashSet(entity.supportedRules()), entity.organizationEligible(), AiCodeFixFeatureEnablement.valueOf(entity.enablement().name()), Sets.newHashSet(entity.enabledProjectKeys())); } public Optional getFeature(Binding binding) { return aiCodeFixRepository.get(binding.connectionId()) .map(AiCodeFixService::aiCodeFixMapping) .filter(settings -> settings.isFeatureEnabled(binding.sonarProjectKey())) .map(AiCodeFixFeature::new); } public SuggestFixResponse suggestFix(String configurationScopeId, UUID issueId, SonarLintCancelMonitor cancelMonitor) { var bindingWithOrg = ensureBound(configurationScopeId); var connection = sonarQubeClientManager.getValidClientOrThrow(bindingWithOrg.binding().connectionId()); var responseBodyDto = connection.withClientApiAndReturn(serverApi -> { var issueOptional = previouslyRaisedFindingsRepository.findRaisedIssueById(issueId); if (issueOptional.isPresent()) { return generateResponseBodyForIssue(serverApi, issueOptional.get(), issueId, bindingWithOrg, cancelMonitor); } else { var taintOptional = taintVulnerabilityTrackingService.getTaintVulnerability(configurationScopeId, issueId, cancelMonitor); if (taintOptional.isPresent()) { return generateResponseBodyForTaint(serverApi, taintOptional.get(), configurationScopeId, bindingWithOrg, cancelMonitor); } else { throw new ResponseErrorException(new ResponseError(ISSUE_NOT_FOUND, "The provided issue or taint does not exist", issueId)); } } }); return adapt(responseBodyDto); } private AiSuggestionResponseBodyDto generateResponseBodyForIssue(ServerApi serverApi, RaisedIssue raisedIssue, UUID issueId, BindingWithOrg bindingWithOrg, SonarLintCancelMonitor cancelMonitor) { var aiCodeFixFeature = getFeature(bindingWithOrg.binding()); if (!aiCodeFixFeature.map(feature -> feature.isFixable(raisedIssue)).orElse(false)) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "The provided issue cannot be fixed", issueId)); } AiSuggestionResponseBodyDto fixResponseDto; try { var requestBody = toDto(bindingWithOrg.organizationKey, bindingWithOrg.binding().sonarProjectKey(), raisedIssue); fixResponseDto = serverApi.fixSuggestions().getAiSuggestion(requestBody, cancelMonitor); } catch (TooManyRequestsException e) { throw new ResponseErrorException(new ResponseError(TOO_MANY_REQUESTS, "AI CodeFix usage has been capped. Too many requests have been made.", issueId)); } eventPublisher.publishEvent(new FixSuggestionReceivedEvent( fixResponseDto.id().toString(), serverApi.isSonarCloud() ? AiSuggestionSource.SONARCLOUD : AiSuggestionSource.SONARQUBE, fixResponseDto.changes().size(), // As of today, this is always true since suggestFix is only called by the clients true)); return fixResponseDto; } private AiSuggestionResponseBodyDto generateResponseBodyForTaint(ServerApi serverApi, TaintVulnerabilityDto taint, String configScopeId, BindingWithOrg bindingWithOrg, SonarLintCancelMonitor cancelMonitor) { var aiCodeFixFeature = getFeature(bindingWithOrg.binding()); if (!aiCodeFixFeature.map(feature -> feature.isFixable(taint)).orElse(false)) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "The provided taint cannot be fixed", taint.getId())); } AiSuggestionResponseBodyDto fixResponseDto; try { var requestBody = toDto(bindingWithOrg.organizationKey, bindingWithOrg.binding().sonarProjectKey(), taint, configScopeId); fixResponseDto = serverApi.fixSuggestions().getAiSuggestion(requestBody, cancelMonitor); } catch (TooManyRequestsException e) { throw new ResponseErrorException(new ResponseError(TOO_MANY_REQUESTS, "AI CodeFix usage has been capped. Too many requests have been made.", taint.getId())); } eventPublisher.publishEvent(new FixSuggestionReceivedEvent( fixResponseDto.id().toString(), serverApi.isSonarCloud() ? AiSuggestionSource.SONARCLOUD : AiSuggestionSource.SONARQUBE, fixResponseDto.changes().size(), // As of today, this is always true since suggestFix is only called by the clients true)); return fixResponseDto; } private static SuggestFixResponse adapt(AiSuggestionResponseBodyDto responseBodyDto) { return new SuggestFixResponse(responseBodyDto.id(), responseBodyDto.explanation(), responseBodyDto.changes().stream().map(change -> new SuggestFixChangeDto(change.startLine(), change.endLine(), change.newCode())).toList()); } private BindingWithOrg ensureBound(String configurationScopeId) { var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); if (effectiveBinding.isEmpty()) { throw new ResponseErrorException(new ResponseError(CONFIG_SCOPE_NOT_BOUND, "The provided configuration scope is not bound", configurationScopeId)); } var binding = effectiveBinding.get(); var connection = connectionRepository.getConnectionById(binding.connectionId()); if (connection == null) { throw new ResponseErrorException(new ResponseError(CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection", configurationScopeId)); } if ((connection instanceof SonarCloudConnectionConfiguration sonarCloudConnection)) { return new BindingWithOrg(sonarCloudConnection.getOrganization(), binding); } return new BindingWithOrg(null, binding); } private AiSuggestionRequestBodyDto toDto(@Nullable String organizationKey, String projectKey, RaisedIssue raisedIssue) { // this is not perfect, the file content might have changed since the issue was detected var clientFile = clientFileSystemService.getClientFile(raisedIssue.fileUri()); if (clientFile == null) { throw new ResponseErrorException(new ResponseError(FILE_NOT_FOUND, "The provided issue ID corresponds to an unknown file", null)); } var issue = raisedIssue.issueDto(); // the text range presence was checked earlier var textRange = requireNonNull(issue.getTextRange()); return new AiSuggestionRequestBodyDto(organizationKey, projectKey, new AiSuggestionRequestBodyDto.Issue(issue.getPrimaryMessage(), textRange.getStartLine(), textRange.getEndLine(), issue.getRuleKey(), clientFile.getContent())); } private AiSuggestionRequestBodyDto toDto(@Nullable String organizationKey, String projectKey, TaintVulnerabilityDto taint, String configScopeId) { ClientFile clientFile = null; var baseDir = clientFileSystemService.getBaseDir(configScopeId); if (baseDir != null) { var fileUri = baseDir.resolve(taint.getIdeFilePath()).toUri(); clientFile = clientFileSystemService.getClientFile(fileUri); } if (clientFile == null) { throw new ResponseErrorException(new ResponseError(FILE_NOT_FOUND, "The provided taint ID corresponds to an unknown file", null)); } // the text range presence was checked earlier var textRange = requireNonNull(taint.getTextRange()); return new AiSuggestionRequestBodyDto(organizationKey, projectKey, new AiSuggestionRequestBodyDto.Issue(taint.getMessage(), textRange.getStartLine(), textRange.getEndLine(), taint.getRuleKey(), clientFile.getContent())); } private record BindingWithOrg(@Nullable String organizationKey, Binding binding) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.remediation.aicodefix; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.reporting; import java.net.URI; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.active.rules.ServerActiveRulesChanged; import org.sonarsource.sonarlint.core.active.rules.StandaloneRulesConfigurationChanged; import org.sonarsource.sonarlint.core.analysis.IssuesRaisedEvent; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.newcode.NewCodeService; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixFeature; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.reporting.PreviouslyRaisedFindingsRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaiseHotspotsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaiseIssuesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; import org.sonarsource.sonarlint.core.tracking.streaming.Alarm; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toMap; import static org.sonarsource.sonarlint.core.DtoMapper.toRaisedHotspotDto; import static org.sonarsource.sonarlint.core.DtoMapper.toRaisedIssueDto; public class FindingReportingService { public static final Duration STREAMING_INTERVAL = Duration.ofMillis(300); private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final NewCodeService newCodeService; private final SeverityModeService severityModeService; private final PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository; private final Map> issuesPerFileUri = new ConcurrentHashMap<>(); private final Map> securityHotspotsPerFileUri = new ConcurrentHashMap<>(); private final Map streamingTriggeringAlarmByConfigScopeId = new ConcurrentHashMap<>(); private final Map> filesPerAnalysis = new ConcurrentHashMap<>(); private final ApplicationEventPublisher eventPublisher; private final boolean isStreamingEnabled; private final AiCodeFixService aiCodeFixService; public FindingReportingService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, NewCodeService newCodeService, SeverityModeService severityModeService, PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, ApplicationEventPublisher eventPublisher, InitializeParams initializeParams, AiCodeFixService aiCodeFixService) { this.client = client; this.configurationRepository = configurationRepository; this.newCodeService = newCodeService; this.severityModeService = severityModeService; this.previouslyRaisedFindingsRepository = previouslyRaisedFindingsRepository; this.eventPublisher = eventPublisher; this.isStreamingEnabled = initializeParams.getBackendCapabilities().contains(BackendCapability.ISSUE_STREAMING); this.aiCodeFixService = aiCodeFixService; } @EventListener public void onStandaloneRulesConfigurationChanged(StandaloneRulesConfigurationChanged event) { if (event.isOnlyDeactivated()) { // if no rules were enabled (only disabled), trigger only a new reporting, removing issues of disabled rules configurationRepository.getConfigScopeIds().stream() .filter(configScopeId -> configurationRepository.getEffectiveBinding(configScopeId).isEmpty()) .forEach(configScopeId -> { var deactivatedRules = event.getDeactivatedRules(); updateAndReportFindings(configScopeId, hotspot -> raisedFindingUpdater(hotspot, deactivatedRules), issue -> raisedFindingUpdater(issue, deactivatedRules)); }); } } @CheckForNull private static T raisedFindingUpdater(T raisedFinding, List deactivatedRules) { if (deactivatedRules.contains(raisedFinding.getRuleKey())) { return null; } return raisedFinding; } @EventListener private void onServerActiveRulesChanged(ServerActiveRulesChanged event) { var deactivatedRules = event.deactivatedRules(); // if rules were activated, an analysis will be triggered in the AnalysisService, and a new reporting will occur if (event.activatedRules().isEmpty() && !deactivatedRules.isEmpty()) { var changedProjectKeys = event.projectKeys(); configurationRepository.getAllBoundScopes().stream() .filter(scope -> event.connectionId().equals(scope.getConnectionId()) && changedProjectKeys.contains(scope.getSonarProjectKey())) .map(BoundScope::getConfigScopeId) .forEach(scopeId -> updateAndReportFindings(scopeId, hotspot -> raisedFindingUpdater(hotspot, deactivatedRules), issue -> raisedFindingUpdater(issue, deactivatedRules))); } } public void resetFindingsForFiles(String configurationScopeId, Set files) { files.forEach(fileUri -> { resetFindingsForFile(issuesPerFileUri, fileUri); resetFindingsForFile(securityHotspotsPerFileUri, fileUri); }); previouslyRaisedFindingsRepository.resetFindingsCache(configurationScopeId, files); } public void initFilesToAnalyze(UUID analysisId, Set files) { filesPerAnalysis.computeIfAbsent(analysisId, k -> new HashSet<>()).addAll(files); } private static void resetFindingsForFile(Map> findingsMap, URI fileUri) { findingsMap.computeIfPresent(fileUri, (k, v) -> List.of()); } public void streamIssue(String configurationScopeId, UUID analysisId, TrackedIssue trackedIssue) { // Cache is cleared on new analysis, but it's possible that 2 analyses almost start at the same time. // In this case, same issues will be reported twice for the same file during the streaming, which will be sent to the client. // A quick workaround is to replace the existing issue with the duplicated one (which should be the most up-to-date). // Ideally, we should be able to cancel the previous analysis if it's not relevant. if (trackedIssue.isSecurityHotspot()) { insertTrackedIssue(securityHotspotsPerFileUri, trackedIssue); } else { insertTrackedIssue(issuesPerFileUri, trackedIssue); } if (isStreamingEnabled) { getStreamingDebounceAlarm(configurationScopeId, analysisId).schedule(); } } private static void insertTrackedIssue(Map> map, TrackedIssue trackedIssue) { map.compute(trackedIssue.getFileUri(), (fileUri, fileFindings) -> { // make sure to return an immutable list as it might be iterated over in parallel if (fileFindings == null) { return List.of(trackedIssue); } var newIssues = new ArrayList<>(fileFindings); newIssues.removeIf(i -> i.getId().equals(trackedIssue.getId())); newIssues.add(trackedIssue); return List.copyOf(newIssues); }); } private void triggerStreaming(String configurationScopeId, UUID analysisId) { var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); var connectionId = effectiveBinding.map(Binding::connectionId).orElse(null); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var aiCodeFixFeature = effectiveBinding.flatMap(aiCodeFixService::getFeature); var issuesToRaise = issuesPerFileUri.entrySet().stream() .filter(e -> filesPerAnalysis.get(analysisId).contains(e.getKey())) .map(e -> Map.entry(e.getKey(), e.getValue().stream().map(issue -> toRaisedIssueDto(issue, newCodeDefinition, isMQRMode, aiCodeFixFeature.map(feature -> feature.isFixable(issue)).orElse(false))) .toList())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); var hotspotsToRaise = securityHotspotsPerFileUri.entrySet().stream() .filter(e -> filesPerAnalysis.get(analysisId).contains(e.getKey())) .map(e -> Map.entry(e.getKey(), e.getValue().stream().map(issue -> toRaisedHotspotDto(issue, newCodeDefinition, isMQRMode)).toList())) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); updateRaisedFindingsCacheAndNotifyClient(configurationScopeId, analysisId, issuesToRaise, hotspotsToRaise, true); } public void reportTrackedFindings(String configurationScopeId, UUID analysisId, Map> issuesToReport, Map> hotspotsToReport) { // stop streaming now, we will raise all issues one last time from this method stopStreaming(configurationScopeId); var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); var connectionId = effectiveBinding.map(Binding::connectionId).orElse(null); var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId).orElseGet(NewCodeDefinition::withAlwaysNew); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var aiCodeFixFeature = effectiveBinding.flatMap(aiCodeFixService::getFeature); var issuesToRaise = getIssuesToRaise(issuesToReport, newCodeDefinition, isMQRMode, aiCodeFixFeature); this.eventPublisher.publishEvent(new IssuesRaisedEvent(issuesToRaise.values().stream().flatMap(List::stream).toList())); var hotspotsToRaise = getHotspotsToRaise(hotspotsToReport, newCodeDefinition, isMQRMode); updateRaisedFindingsCacheAndNotifyClient(configurationScopeId, analysisId, issuesToRaise, hotspotsToRaise, false); filesPerAnalysis.remove(analysisId); } private synchronized void updateRaisedFindingsCacheAndNotifyClient(String configurationScopeId, @Nullable UUID analysisId, Map> updatedIssues, Map> updatedHotspots, boolean isIntermediatePublication) { var fileIssues = previouslyRaisedFindingsRepository.replaceIssuesForFiles(configurationScopeId, updatedIssues); var totalIssues = fileIssues.values().stream().mapToInt(List::size).sum(); LOG.debug("Reporting {} issues over {} files for configuration scope {}", totalIssues, fileIssues.size(), configurationScopeId); client.raiseIssues(new RaiseIssuesParams(configurationScopeId, fileIssues, isIntermediatePublication, analysisId)); var effectiveBindingOpt = configurationRepository.getEffectiveBinding(configurationScopeId); if (effectiveBindingOpt.isPresent()) { // security hotspots are only supported in connected mode var hotspotsToRaise = previouslyRaisedFindingsRepository.replaceHotspotsForFiles(configurationScopeId, updatedHotspots); client.raiseHotspots(new RaiseHotspotsParams(configurationScopeId, hotspotsToRaise, isIntermediatePublication, analysisId)); } } private void stopStreaming(String configurationScopeId) { var alarm = removeStreamingDebounceAlarmIfExists(configurationScopeId); if (alarm != null) { alarm.shutdownNow(); } } private Alarm getStreamingDebounceAlarm(String configurationScopeId, UUID analysisId) { return streamingTriggeringAlarmByConfigScopeId.computeIfAbsent(configurationScopeId, id -> new Alarm("sonarlint-finding-streamer", STREAMING_INTERVAL, () -> triggerStreaming(configurationScopeId, analysisId))); } private Alarm removeStreamingDebounceAlarmIfExists(String configurationScopeId) { return streamingTriggeringAlarmByConfigScopeId.remove(configurationScopeId); } private static Map> getIssuesToRaise(Map> updatedIssues, NewCodeDefinition newCodeDefinition, boolean isMQRMode, Optional aiCodeFixFeature) { LOG.debug("AiCodeFix optional is present: {}", aiCodeFixFeature.isPresent()); return updatedIssues.values().stream().flatMap(Collection::stream) .collect(groupingBy(TrackedIssue::getFileUri, Collectors.mapping(issue -> toRaisedIssueDto(issue, newCodeDefinition, isMQRMode, aiCodeFixFeature.map(feature -> { LOG.debug("AiCodeFix is fixable: {}", aiCodeFixFeature.get().isFixable(issue)); LOG.debug("Supported rules: {}", aiCodeFixFeature.get().settings().supportedRules()); LOG.debug("Issue ruleKey {} and text range {}", issue.getRuleKey(), issue.getTextRangeWithHash()); return feature.isFixable(issue); }).orElse(false)), Collectors.toList()))); } private static Map> getHotspotsToRaise(Map> hotspots, NewCodeDefinition newCodeDefinition, boolean isMQRMode) { return hotspots.values().stream().flatMap(Collection::stream) .collect(groupingBy(TrackedIssue::getFileUri, Collectors.mapping(hotspot -> toRaisedHotspotDto(hotspot, newCodeDefinition, isMQRMode), Collectors.toList()))); } public void updateAndReportIssues(String configurationScopeId, UnaryOperator issueUpdater) { updateAndReportFindings(configurationScopeId, UnaryOperator.identity(), issueUpdater); } public void updateAndReportHotspots(String configurationScopeId, UnaryOperator hotspotUpdater) { updateAndReportFindings(configurationScopeId, hotspotUpdater, UnaryOperator.identity()); } public void updateAndReportFindings(String configurationScopeId, UnaryOperator hotspotUpdater, UnaryOperator issueUpdater) { var updatedHotspots = updateFindings(hotspotUpdater, previouslyRaisedFindingsRepository.getRaisedHotspotsForScope(configurationScopeId)); var updatedIssues = updateFindings(issueUpdater, previouslyRaisedFindingsRepository.getRaisedIssuesForScope(configurationScopeId)); updateRaisedFindingsCacheAndNotifyClient(configurationScopeId, null, updatedIssues, updatedHotspots, false); } private static Map> updateFindings(UnaryOperator findingUpdater, Map> previouslyRaisedFindings) { Map> updatedFindings = new HashMap<>(); previouslyRaisedFindings.forEach((uri, finding) -> { var updatedFindingsForFile = finding.stream() .map(findingUpdater) .filter(Objects::nonNull) .toList(); updatedFindings.put(uri, updatedFindingsForFile); }); return updatedFindings; } @CheckForNull public RaisedIssueDto findReportedIssue(UUID issueId, NewCodeDefinition newCodeDefinition, boolean isMQRMode, Optional aiCodeFixFeature) { for (var findingsForFile : issuesPerFileUri.values()) { var optFinding = findingsForFile.stream().filter(issue -> issue.getId().equals(issueId)).findFirst(); if (optFinding.isPresent()) { return toRaisedIssueDto(optFinding.get(), newCodeDefinition, isMQRMode, aiCodeFixFeature.map(feature -> feature.isFixable(optFinding.get())).orElse(false)); } } return null; } @CheckForNull public RaisedHotspotDto findReportedHotspot(UUID hotspotId, NewCodeDefinition newCodeDefinition, boolean isMQRMode) { for (var findingsForFile : securityHotspotsPerFileUri.values()) { var optFinding = findingsForFile.stream().filter(hotspot -> hotspot.getId().equals(hotspotId)).findFirst(); if (optFinding.isPresent()) { return toRaisedHotspotDto(optFinding.get(), newCodeDefinition, isMQRMode); } } return null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.reporting; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/BindingConfiguration.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; import java.util.Optional; import java.util.function.BiFunction; import javax.annotation.Nullable; public record BindingConfiguration(@Nullable String connectionId, @Nullable String sonarProjectKey, boolean bindingSuggestionDisabled) { public static BindingConfiguration noBinding() { return noBinding(false); } public static BindingConfiguration noBinding(boolean bindingSuggestionDisabled) { return new BindingConfiguration(null, null, bindingSuggestionDisabled); } public boolean isBound() { return connectionId != null && sonarProjectKey != null; } public Optional ifBound(BiFunction calledIfBound) { if (isBound()) { return Optional.of(calledIfBound.apply(connectionId, sonarProjectKey)); } return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toSet; public class ConfigurationRepository { private final Map configScopeById = new ConcurrentHashMap<>(); private final Map bindingByConfigScopeId = new ConcurrentHashMap<>(); public ConfigurationScope addOrReplace(ConfigurationScope configScope, BindingConfiguration bindingConfig) { var id = configScope.id(); var previous = configScopeById.put(id, configScope); bindingByConfigScopeId.put(id, bindingConfig); return previous; } @CheckForNull public ConfigurationScopeWithBinding remove(String idToRemove) { var removedScope = configScopeById.remove(idToRemove); var removeBindingConfiguration = bindingByConfigScopeId.remove(idToRemove); return removedScope == null ? null : new ConfigurationScopeWithBinding(removedScope, removeBindingConfiguration); } public Map removeBindingForConnection(String connectionId) { var removedBindingByConfigScope = new HashMap(); var configScopeIdsToUnbind = bindingByConfigScopeId.entrySet().stream().filter(e -> connectionId.equals(e.getValue().connectionId())).map(Map.Entry::getKey).collect(toSet()); configScopeIdsToUnbind.forEach(configScopeId -> { var removedBindingConfiguration = bindingByConfigScopeId.get(configScopeId); if (removedBindingConfiguration != null) { var noBinding = BindingConfiguration.noBinding(removedBindingConfiguration.bindingSuggestionDisabled()); updateBinding(configScopeId, noBinding); removedBindingByConfigScope.put(configScopeId, removedBindingConfiguration); } }); return removedBindingByConfigScope; } public void updateBinding(String configScopeId, BindingConfiguration bindingConfig) { bindingByConfigScopeId.put(configScopeId, bindingConfig); } public Set getConfigScopeIds() { return Set.copyOf(configScopeById.keySet()); } @CheckForNull public BindingConfiguration getBindingConfiguration(String configScopeId) { return bindingByConfigScopeId.get(configScopeId); } public Optional getEffectiveBinding(String configScopeId) { var configScopeIdToSearchIn = requireNonNull(configScopeId, "Configuration Scope ID is mandatory"); while (true) { var binding = getConfiguredBinding(configScopeIdToSearchIn); if (binding.isPresent()) { return binding; } var parentId = getParentId(configScopeIdToSearchIn); if (parentId.isEmpty()) { return Optional.empty(); } configScopeIdToSearchIn = parentId.get(); } } public Binding getEffectiveBindingOrThrow(String configScopeId) { return getEffectiveBinding(configScopeId).orElseThrow(() -> { var error = new ResponseError(SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND, "No binding for config scope '" + configScopeId + "'", configScopeId); return new ResponseErrorException(error); }); } public Optional getConfiguredBinding(String configScopeId) { var bindingConfiguration = bindingByConfigScopeId.get(configScopeId); if (bindingConfiguration != null && bindingConfiguration.isBound()) { return Optional.of(new Binding(requireNonNull(bindingConfiguration.connectionId()), requireNonNull(bindingConfiguration.sonarProjectKey()))); } return Optional.empty(); } private Optional getParentId(String configScopeId) { var configurationScope = configScopeById.get(configScopeId); if (configurationScope != null) { return Optional.ofNullable(configurationScope.parentId()); } return Optional.empty(); } public Set getLeafConfigScopeIds() { var leafConfigScopeIds = new HashSet<>(configScopeById.keySet()); configScopeById.values().forEach(scope -> { var parentId = scope.parentId(); if (parentId != null) { leafConfigScopeIds.remove(parentId); } }); return leafConfigScopeIds; } public boolean isLeafConfigScope(String configScopeId) { return getLeafConfigScopeIds().contains(configScopeId); } @CheckForNull public ConfigurationScope getConfigurationScope(String configScopeId) { return configScopeById.get(configScopeId); } public Collection getAllBoundScopes() { return configScopeById.keySet() .stream() .map(scopeId -> { var effectiveBinding = getEffectiveBinding(scopeId); return effectiveBinding.map(binding -> new BoundScope(scopeId, binding)).orElse(null); }) .filter(Objects::nonNull) .toList(); } public Collection getAllBindableUnboundScopes() { return configScopeById.entrySet() .stream() .filter(e -> e.getValue().bindable()) .filter(e -> getEffectiveBinding(e.getKey()).isEmpty()) .map(Map.Entry::getValue) .toList(); } @CheckForNull public BoundScope getBoundScope(String configScopeId) { var effectiveBinding = getEffectiveBinding(configScopeId); return effectiveBinding.map(binding -> new BoundScope(configScopeId, binding)).orElse(null); } public Collection getBoundScopesToConnectionAndSonarProject(String connectionId, String projectKey) { return getBoundScopesToConnection(connectionId) .stream() .filter(b -> projectKey.equals(b.getSonarProjectKey())) .toList(); } public Collection getBoundScopesToConnection(String connectionId) { return getAllBoundScopes() .stream() .filter(b -> connectionId.equals(b.getConnectionId())) .toList(); } public boolean hasScopesBoundToConnection(String connectionId) { return !getBoundScopesToConnection(connectionId).isEmpty(); } /** * Return the set of Sonar Project keys used in at least one binding for the given connection. */ public Set getSonarProjectsUsedForConnection(String connectionId) { return getAllBoundScopes() .stream() .filter(b -> connectionId.equals(b.getConnectionId())) .map(BoundScope::getSonarProjectKey) .collect(toSet()); } public Map>> getBoundScopeByConnectionAndSonarProject() { return getAllBoundScopes() .stream() .collect(groupingBy(BoundScope::getConnectionId, groupingBy(BoundScope::getSonarProjectKey, Collectors.toCollection(ArrayList::new)))); } public List getChildrenWithInheritedBinding(String parentId) { return configScopeById.values().stream() .filter(scope -> parentId.equals(scope.parentId()) && (!bindingByConfigScopeId.containsKey(scope.id()) || !bindingByConfigScopeId.get(scope.id()).isBound())) .map(ConfigurationScope::id) .toList(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationScope.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; import javax.annotation.Nullable; /** * @param name The name of this configuration scope. Used for auto-binding. */ public record ConfigurationScope(String id, @Nullable String parentId, boolean bindable, String name) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationScopeWithBinding.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; public record ConfigurationScopeWithBinding(ConfigurationScope scope, BindingConfiguration bindingConfiguration) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/config/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.repository.config; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/AbstractConnectionConfiguration.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import java.net.URI; import java.net.URISyntaxException; import java.util.Objects; import org.apache.commons.lang3.Strings; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; public abstract class AbstractConnectionConfiguration { /** * The id of the connection on the client side */ private final String connectionId; private final boolean disableNotifications; private final ConnectionKind kind; private final String url; protected AbstractConnectionConfiguration(String connectionId, ConnectionKind kind, boolean disableNotifications, String url) { Objects.requireNonNull(connectionId, "Connection id is mandatory"); this.connectionId = connectionId; this.kind = kind; this.disableNotifications = disableNotifications; this.url = Strings.CS.removeEnd(url, "/"); } public String getConnectionId() { return connectionId; } public ConnectionKind getKind() { return kind; } public boolean isDisableNotifications() { return disableNotifications; } public String getUrl() { return url; } public abstract EndpointParams getEndpointParams(); public boolean isSameServerUrl(String otherUrl) { URI myUri; URI otherUri; try { myUri = new URI(Strings.CS.removeEnd(url, "/")); otherUri = new URI(Strings.CS.removeEnd(otherUrl, "/")); } catch (URISyntaxException e) { return false; } return Objects.equals(myUri, otherUri); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; var that = (AbstractConnectionConfiguration) o; return Objects.equals(connectionId, that.connectionId) && Objects.equals(disableNotifications, that.disableNotifications) && Objects.equals(url, that.url); } @Override public int hashCode() { return Objects.hash(connectionId, url); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/ConnectionConfigurationRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; public class ConnectionConfigurationRepository { private final Map connectionsById = new ConcurrentHashMap<>(); /** * Add or replace connection configuration. * @return the previous configuration with the same id, if any */ @CheckForNull public AbstractConnectionConfiguration addOrReplace(AbstractConnectionConfiguration connectionConfiguration) { return connectionsById.put(connectionConfiguration.getConnectionId(), connectionConfiguration); } /** * Remove a connection configuration. * @return the removed configuration, if any */ @CheckForNull public AbstractConnectionConfiguration remove(String idToRemove) { return connectionsById.remove(idToRemove); } public Map getConnectionsById() { return Map.copyOf(connectionsById); } @CheckForNull public AbstractConnectionConfiguration getConnectionById(String id) { return connectionsById.get(id); } public Optional getEndpointParams(String connectionId) { return Optional.ofNullable(getConnectionById(connectionId)).map(AbstractConnectionConfiguration::getEndpointParams); } public boolean hasConnectionWithOrigin(String serverOrigin) { // The Origin header has the following format: ://(:) // Since servers can have an optional "context path" after this, we consider a valid match when the server's configured URL begins with the // passed Origin // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin return connectionsById.values().stream() .anyMatch(connection -> haveSameOrigin(connection.getUrl(), serverOrigin)); } public static boolean haveSameOrigin(String knownServerUrl, String incomingOrigin) { return ensureTrailingSlash(knownServerUrl).startsWith(ensureTrailingSlash(incomingOrigin)); } private static String ensureTrailingSlash(String s) { return !s.endsWith("/") ? (s + "/") : s; } public List findByUrl(String serverUrl) { return connectionsById.values().stream() .filter(connection -> connection.isSameServerUrl(serverUrl)) .toList(); } public List findByOrganization(String organization) { return connectionsById.values().stream() .filter(connection -> connection.getKind() == ConnectionKind.SONARCLOUD) .filter(scConnection -> ((SonarCloudConnectionConfiguration) scConnection).getOrganization().equals(organization)) .toList(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfiguration.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import java.net.URI; import java.util.Objects; import javax.annotation.Nullable; import org.apache.commons.lang3.Strings; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; public class SonarCloudConnectionConfiguration extends AbstractConnectionConfiguration { private final URI apiUri; private final String organization; private final SonarCloudRegion region; public SonarCloudConnectionConfiguration(URI uri, URI apiUri, String connectionId, String organization, SonarCloudRegion region, boolean disableNotifications) { super(connectionId, ConnectionKind.SONARCLOUD, disableNotifications, uri.toString()); this.apiUri = apiUri; this.organization = organization; this.region = region; } public String getOrganization() { return organization; } @Override public EndpointParams getEndpointParams() { return new EndpointParams(getUrl(), Strings.CS.removeEnd(apiUri.toString(), "/"), true, organization); } public SonarCloudRegion getRegion() { return region; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } if (!super.equals(o)) { return false; } var that = (SonarCloudConnectionConfiguration) o; return Objects.equals(organization, that.organization); } @Override public int hashCode() { return Objects.hash(super.hashCode(), organization); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfiguration.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; public class SonarQubeConnectionConfiguration extends AbstractConnectionConfiguration { public SonarQubeConnectionConfiguration(String connectionId, String serverUrl, boolean disableNotifications) { super(connectionId, ConnectionKind.SONARQUBE, disableNotifications, serverUrl); } @Override public EndpointParams getEndpointParams() { return new EndpointParams(getUrl(), null, false, null); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/connection/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.repository.connection; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.reporting; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; public class PreviouslyRaisedFindingsRepository { private final Map>> previouslyRaisedIssuesByScopeId = new ConcurrentHashMap<>(); private final Map>> previouslyRaisedHotspotsByScopeId = new ConcurrentHashMap<>(); public Map> replaceIssuesForFiles(String scopeId, Map> raisedIssues) { return addOrReplaceFindings(scopeId, raisedIssues, previouslyRaisedIssuesByScopeId); } public Map> replaceHotspotsForFiles(String scopeId, Map> raisedHotpots) { return addOrReplaceFindings(scopeId, raisedHotpots, previouslyRaisedHotspotsByScopeId); } private static Map> addOrReplaceFindings(String scopeId, Map> raisedFindings, Map>> previouslyRaisedFindingsByScopeId) { var findingsPerFile = previouslyRaisedFindingsByScopeId.computeIfAbsent(scopeId, k -> new ConcurrentHashMap<>()); findingsPerFile.putAll(raisedFindings); return findingsPerFile; } public Map> getRaisedIssuesForScope(String scopeId) { return previouslyRaisedIssuesByScopeId.getOrDefault(scopeId, Map.of()); } public Map> getRaisedHotspotsForScope(String scopeId) { return previouslyRaisedHotspotsByScopeId.getOrDefault(scopeId, Map.of()); } public void resetFindingsCache(String scopeId, Set files) { resetCacheForFindings(scopeId, files, previouslyRaisedIssuesByScopeId); resetCacheForFindings(scopeId, files, previouslyRaisedHotspotsByScopeId); } private static void resetCacheForFindings(String scopeId, Set files, Map>> cache) { Map> blankCache = files.stream().collect(Collectors.toMap(Function.identity(), e -> new ArrayList<>())); cache.compute(scopeId, (file, issues) -> blankCache); } public Optional findRaisedIssueById(UUID issueId) { return previouslyRaisedIssuesByScopeId.values().stream() .flatMap(issuesByUri -> issuesByUri.entrySet().stream() .flatMap(entry -> entry.getValue().stream().filter(issue -> issue.getId().equals(issueId)).findFirst().map(issue -> new RaisedIssue(entry.getKey(), issue)).stream())) .findFirst(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/RaisedIssue.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.reporting; import java.net.URI; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; public record RaisedIssue(URI fileUri, RaisedIssueDto issueDto) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.repository.reporting; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/rules/RulesRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.rules; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.rules.RulesExtractionHelper; import org.sonarsource.sonarlint.core.serverconnection.ServerSettings; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.storage.StorageService; public class RulesRepository { private final SonarLintLogger logger = SonarLintLogger.get(); private final RulesExtractionHelper extractionHelper; private Map embeddedRulesByKey; private final Map> rulesByKeyByConnectionId = new HashMap<>(); private final Map> ruleKeyReplacementsByConnectionId = new HashMap<>(); private final StorageService storageService; public RulesRepository(RulesExtractionHelper extractionHelper, StorageService storageService) { this.extractionHelper = extractionHelper; this.storageService = storageService; } public synchronized Collection getEmbeddedRules() { lazyInit(); return embeddedRulesByKey.values(); } public synchronized Optional getEmbeddedRule(String ruleKey) { lazyInit(); return Optional.ofNullable(embeddedRulesByKey.get(ruleKey)); } private synchronized void lazyInit() { if (embeddedRulesByKey == null) { this.embeddedRulesByKey = byKey(extractionHelper.extractEmbeddedRules()); } } public synchronized Optional getRule(String connectionId, String ruleKey) { lazyInit(connectionId); var connectionRules = rulesByKeyByConnectionId.get(connectionId); return Optional.ofNullable(connectionRules.get(ruleKey)) .or(() -> Optional.ofNullable(connectionRules.get(ruleKeyReplacementsByConnectionId.get(connectionId).get(ruleKey)))); } private synchronized void lazyInit(String connectionId) { var rulesByKey = rulesByKeyByConnectionId.get(connectionId); if (rulesByKey == null) { var serverSettings = storageService.connection(connectionId).serverInfo().read().map(StoredServerInfo::globalSettings); setRules(connectionId, extractionHelper.extractRulesForConnection(connectionId, serverSettings.map(ServerSettings::globalSettings).orElseGet(Map::of))); } } private void setRules(String connectionId, Collection rules) { var rulesByKey = byKey(rules); var ruleKeyReplacements = new HashMap(); rules.forEach(rule -> rule.getDeprecatedKeys().forEach(deprecatedKey -> ruleKeyReplacements.put(deprecatedKey, rule.getKey()))); rulesByKeyByConnectionId.put(connectionId, rulesByKey); ruleKeyReplacementsByConnectionId.put(connectionId, ruleKeyReplacements); } private static Map byKey(Collection rules) { return rules.stream() .collect(Collectors.toMap(SonarLintRuleDefinition::getKey, r -> r)); } public synchronized void evictFor(String connectionId) { logger.debug("Evict cached rules definitions for connection '{}'", connectionId); rulesByKeyByConnectionId.remove(connectionId); ruleKeyReplacementsByConnectionId.remove(connectionId); } public synchronized void evictEmbedded() { logger.debug("Evict cached embedded rules definitions"); embeddedRulesByKey = null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/rules/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.repository.rules; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/CleanCodePrinciples.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.io.IOException; import java.nio.charset.StandardCharsets; import javax.annotation.CheckForNull; import org.apache.commons.io.IOUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; class CleanCodePrinciples { @CheckForNull public static String getContent(String key) { var fileStream = CleanCodePrinciples.class.getResourceAsStream("/clean-code-principles/" + key + ".html"); if (fileStream == null) { SonarLintLogger.get().info("Unsupported clean code principle key: " + key); return null; } try { return IOUtils.toString(fileStream, StandardCharsets.UTF_8).trim().replaceAll("\\r\\n?", "\n"); } catch (IOException e) { SonarLintLogger.get().error("Could not read content for clean code principle key: " + key, e); return null; } } private CleanCodePrinciples() { // utility class } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/OthersSectionHtmlContent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class OthersSectionHtmlContent { private static final String FOLDER_NAME = "/context-rule-description/"; private static final String FILE_EXTENSION = ".html"; private static final String UNSUPPORTED_RULE_DESCRIPTION_FOR_CONTEXT_KEY = "Unsupported rule description for context key: "; private static final String ERROR_READING_FILE_CONTENT = "Could not read the content for rule description for context key: "; private static final String OTHERS_SECTION_HTML_CONTENT_KEY = "others_section_html_content"; private OthersSectionHtmlContent() {} public static String getHtmlContent() { try (var htmlContentFile = OthersSectionHtmlContent.class.getResourceAsStream(FOLDER_NAME + OTHERS_SECTION_HTML_CONTENT_KEY + FILE_EXTENSION)) { if (htmlContentFile == null) { SonarLintLogger.get().info(UNSUPPORTED_RULE_DESCRIPTION_FOR_CONTEXT_KEY + OTHERS_SECTION_HTML_CONTENT_KEY); return ""; } return IOUtils.toString(htmlContentFile, StandardCharsets.UTF_8).trim().replaceAll("\\r\\n?", "\n"); } catch (IOException ioException) { SonarLintLogger.get().error(ERROR_READING_FILE_CONTENT + OTHERS_SECTION_HTML_CONTENT_KEY, ioException); return ""; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RuleDetails.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.StandaloneRuleConfigDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleParamDefinition; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import org.sonarsource.sonarlint.core.serverapi.rules.ServerActiveRule; import org.sonarsource.sonarlint.core.serverapi.rules.ServerRule; public class RuleDetails { public static final String DEFAULT_SECTION = "default"; private final String key; private final SonarLanguage language; private final String name; private final String htmlDescription; private final Map> descriptionSectionsByKey; private final IssueSeverity defaultSeverity; private final RuleType type; private final CleanCodeAttribute cleanCodeAttribute; private final Map impacts; private final Collection params; private final String extendedDescription; private final Set educationPrincipleKeys; private final VulnerabilityProbability vulnerabilityProbability; public RuleDetails(String key, SonarLanguage language, String name, String htmlDescription, Map> descriptionSectionsByKey, Map impacts, @Nullable IssueSeverity defaultSeverity, @Nullable RuleType type, @Nullable CleanCodeAttribute cleanCodeAttribute, @Nullable String extendedDescription, Collection params, Set educationPrincipleKeys, @Nullable VulnerabilityProbability vulnerabilityProbability) { this.key = key; this.language = language; this.name = name; this.htmlDescription = htmlDescription; this.descriptionSectionsByKey = descriptionSectionsByKey; this.defaultSeverity = defaultSeverity; this.type = type; this.cleanCodeAttribute = cleanCodeAttribute; this.impacts = impacts; this.params = params; this.extendedDescription = extendedDescription; this.educationPrincipleKeys = educationPrincipleKeys; this.vulnerabilityProbability = vulnerabilityProbability; } public static RuleDetails from(SonarLintRuleDefinition ruleDefinition, @Nullable StandaloneRuleConfigDto ruleConfig) { return new RuleDetails( ruleDefinition.getKey(), ruleDefinition.getLanguage(), ruleDefinition.getName(), ruleDefinition.getHtmlDescription(), ruleDefinition.getDescriptionSections().stream() .map(s -> new DescriptionSection(s.getKey(), s.getHtmlContent(), s.getContext().map(c -> new DescriptionSection.Context(c.getKey(), c.getDisplayName())))) .collect(Collectors.groupingBy(DescriptionSection::getKey)), ruleDefinition.getDefaultImpacts(), ruleDefinition.getDefaultSeverity(), ruleDefinition.getType(), ruleDefinition.getCleanCodeAttribute().orElse(CleanCodeAttribute.defaultCleanCodeAttribute()), null, transformParams(ruleDefinition.getParams(), ruleConfig != null ? ruleConfig.getParamValueByKey() : Map.of()), ruleDefinition.getEducationPrincipleKeys(), ruleDefinition.getVulnerabilityProbability().orElse(null)); } @NotNull private static List transformParams(Map ruleDefinitionParams, Map ruleConfigParams) { return ruleDefinitionParams.values() .stream() .map(p -> new EffectiveRuleParam(p.name(), p.description(), ruleConfigParams.getOrDefault(p.key(), p.defaultValue()), p.defaultValue())) .toList(); } public static RuleDetails merging(ServerActiveRule activeRuleFromStorage, ServerRule serverRule) { return new RuleDetails(activeRuleFromStorage.getRuleKey(), serverRule.getLanguage(), serverRule.getName(), serverRule.getHtmlDesc(), serverRule.getDescriptionSections().stream() .map(s -> new DescriptionSection(s.getKey(), s.getHtmlContent(), s.getContext().map(c -> new DescriptionSection.Context(c.getKey(), c.getDisplayName())))) .collect(Collectors.groupingBy(DescriptionSection::getKey)), serverRule.getImpacts(), Optional.ofNullable(activeRuleFromStorage.getSeverity()).orElse(serverRule.getSeverity()), serverRule.getType(), serverRule.getCleanCodeAttribute(), serverRule.getHtmlNote(), Collections.emptyList(), serverRule.getEducationPrincipleKeys(), // TODO get vulnerability probability from storage? null); } public static RuleDetails merging(ServerRule activeRuleFromServer, SonarLintRuleDefinition ruleDefFromPlugin, boolean skipCleanCodeTaxonomy) { var cleanCodeAttribute = skipCleanCodeTaxonomy ? null : ruleDefFromPlugin.getCleanCodeAttribute().orElse(CleanCodeAttribute.defaultCleanCodeAttribute()); var defaultImpacts = skipCleanCodeTaxonomy ? Map.of() : ruleDefFromPlugin.getDefaultImpacts(); return new RuleDetails(ruleDefFromPlugin.getKey(), ruleDefFromPlugin.getLanguage(), ruleDefFromPlugin.getName(), ruleDefFromPlugin.getHtmlDescription(), ruleDefFromPlugin.getDescriptionSections().stream() .map(s -> new DescriptionSection(s.getKey(), s.getHtmlContent(), s.getContext().map(c -> new DescriptionSection.Context(c.getKey(), c.getDisplayName())))) .collect(Collectors.groupingBy(DescriptionSection::getKey)), defaultImpacts, Optional.ofNullable(activeRuleFromServer.getSeverity()).orElse(ruleDefFromPlugin.getDefaultSeverity()), ruleDefFromPlugin.getType(), cleanCodeAttribute, activeRuleFromServer.getHtmlNote(), Collections.emptyList(), ruleDefFromPlugin.getEducationPrincipleKeys(), ruleDefFromPlugin.getVulnerabilityProbability().orElse(null)); } public static RuleDetails merging(ServerActiveRule activeRuleFromStorage, ServerRule serverRule, SonarLintRuleDefinition templateRuleDefFromPlugin, boolean skipCleanCodeTaxonomy) { var cleanCodeAttribute = skipCleanCodeTaxonomy ? null : templateRuleDefFromPlugin.getCleanCodeAttribute().orElse(CleanCodeAttribute.defaultCleanCodeAttribute()); var defaultImpacts = skipCleanCodeTaxonomy ? Map.of() : templateRuleDefFromPlugin.getDefaultImpacts(); return new RuleDetails( activeRuleFromStorage.getRuleKey(), templateRuleDefFromPlugin.getLanguage(), serverRule.getName(), serverRule.getHtmlDesc(), serverRule.getDescriptionSections().stream() .map(s -> new DescriptionSection(s.getKey(), s.getHtmlContent(), s.getContext().map(c -> new DescriptionSection.Context(c.getKey(), c.getDisplayName())))) .collect(Collectors.groupingBy(DescriptionSection::getKey)), mergeImpacts(defaultImpacts, activeRuleFromStorage.getOverriddenImpacts()), serverRule.getSeverity(), serverRule.getType(), cleanCodeAttribute, serverRule.getHtmlNote(), Collections.emptyList(), templateRuleDefFromPlugin.getEducationPrincipleKeys(), templateRuleDefFromPlugin.getVulnerabilityProbability().orElse(null)); } public static Map mergeImpacts(Map defaultImpacts, List overriddenImpacts) { var mergedImpacts = new EnumMap(SoftwareQuality.class); if (!defaultImpacts.isEmpty()) { mergedImpacts = new EnumMap<>(defaultImpacts); } for (var impact : overriddenImpacts) { var quality = SoftwareQuality.valueOf(impact.getSoftwareQuality()); var severity = ImpactSeverity.mapSeverity(impact.getSeverity()); mergedImpacts.put(quality, severity); } return Collections.unmodifiableMap(mergedImpacts); } public static RuleDetails merging(RuleDetails serverActiveRuleDetails, RaisedFindingDto raisedFindingDto) { var isMQRMode = raisedFindingDto.getSeverityMode().isRight(); var softwareImpacts = new EnumMap(SoftwareQuality.class); if (isMQRMode) { raisedFindingDto.getSeverityMode().getRight().getImpacts().forEach( i -> softwareImpacts.put(SoftwareQuality.valueOf(i.getSoftwareQuality().name()), ImpactSeverity.valueOf(i.getImpactSeverity().name())) ); } return new RuleDetails(serverActiveRuleDetails.getKey(), serverActiveRuleDetails.getLanguage(), serverActiveRuleDetails.getName(), serverActiveRuleDetails.getHtmlDescription(), serverActiveRuleDetails.getDescriptionSectionsByKey(), softwareImpacts, isMQRMode ? null : IssueSeverity.valueOf(raisedFindingDto.getSeverityMode().getLeft().getSeverity().toString()), isMQRMode ? null : RuleType.valueOf(raisedFindingDto.getSeverityMode().getLeft().getType().toString()), isMQRMode ? CleanCodeAttribute.valueOf(raisedFindingDto.getSeverityMode().getRight().getCleanCodeAttribute().name()) : null, serverActiveRuleDetails.getExtendedDescription(), serverActiveRuleDetails.getParams(), serverActiveRuleDetails.educationPrincipleKeys, serverActiveRuleDetails.getVulnerabilityProbability()); } public static RuleDetails merging(RuleDetails serverActiveRuleDetails, TaintVulnerabilityDto taintVulnerabilityDto) { var isMQRMode = taintVulnerabilityDto.getSeverityMode().isRight(); EnumMap softwareImpacts = new EnumMap<>(SoftwareQuality.class); if (isMQRMode) { taintVulnerabilityDto.getSeverityMode().getRight().getImpacts().forEach( i -> softwareImpacts.put(SoftwareQuality.valueOf(i.getSoftwareQuality().name()), ImpactSeverity.valueOf(i.getImpactSeverity().name())) ); } return new RuleDetails(serverActiveRuleDetails.getKey(), serverActiveRuleDetails.getLanguage(), serverActiveRuleDetails.getName(), serverActiveRuleDetails.getHtmlDescription(), serverActiveRuleDetails.getDescriptionSectionsByKey(), softwareImpacts, isMQRMode ? null : IssueSeverity.valueOf(taintVulnerabilityDto.getSeverityMode().getLeft().getSeverity().toString()), isMQRMode ? null : RuleType.valueOf(taintVulnerabilityDto.getSeverityMode().getLeft().getType().toString()), isMQRMode ? CleanCodeAttribute.valueOf(taintVulnerabilityDto.getSeverityMode().getRight().getCleanCodeAttribute().name()) : null, serverActiveRuleDetails.getExtendedDescription(), serverActiveRuleDetails.getParams(), serverActiveRuleDetails.educationPrincipleKeys, serverActiveRuleDetails.getVulnerabilityProbability()); } public String getKey() { return key; } public SonarLanguage getLanguage() { return language; } public String getName() { return name; } public String getHtmlDescription() { return htmlDescription; } public boolean hasMonolithicDescription() { return descriptionSectionsByKey.isEmpty() || hasOnlyDefaultSection(); } private boolean hasOnlyDefaultSection() { return descriptionSectionsByKey.size() == 1 && descriptionSectionsByKey.containsKey(DEFAULT_SECTION); } public Map> getDescriptionSectionsByKey() { return descriptionSectionsByKey; } @CheckForNull public IssueSeverity getDefaultSeverity() { return defaultSeverity; } @CheckForNull public RuleType getType() { return type; } public Optional getCleanCodeAttribute() { return Optional.ofNullable(cleanCodeAttribute); } public Map getImpacts() { return impacts; } public Collection getParams() { return params; } public Set getCleanCodePrincipleKeys() { return educationPrincipleKeys; } @CheckForNull public String getExtendedDescription() { return extendedDescription; } public VulnerabilityProbability getVulnerabilityProbability() { return vulnerabilityProbability; } public static class EffectiveRuleParam { private final String name; private final String description; @Nullable private final String value; @Nullable private final String defaultValue; public EffectiveRuleParam(String name, String description, @Nullable String value, @Nullable String defaultValue) { this.name = name; this.description = description; this.value = value; this.defaultValue = defaultValue; } public String getName() { return name; } public String getDescription() { return description; } @CheckForNull public String getValue() { return value; } @CheckForNull public String getDefaultValue() { return defaultValue; } } public static class DescriptionSection { private final String key; private final String htmlContent; private final Optional context; public DescriptionSection(String key, String htmlContent, Optional context) { this.key = key; this.htmlContent = htmlContent; this.context = context; } public String getKey() { return key; } public String getHtmlContent() { return htmlContent; } public Optional getContext() { return context; } public static class Context { private final String key; private final String displayName; public Context(String key, String displayName) { this.key = key; this.displayName = displayName; } public String getKey() { return key; } public String getDisplayName() { return displayName; } } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RuleDetailsAdapter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFileEdit; import org.sonarsource.sonarlint.core.analysis.api.Flow; import org.sonarsource.sonarlint.core.analysis.api.QuickFix; import org.sonarsource.sonarlint.core.analysis.api.TextEdit; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.EffectiveRuleDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.EffectiveRuleParamDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleContextualSectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleContextualSectionWithDefaultContextKeyDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDescriptionTabDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleMonolithicDescriptionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleNonContextualSectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleSplitDescriptionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.VulnerabilityProbability; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.FileEditDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueFlowDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueLocationDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.QuickFixDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.TextEditDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttributeCategory; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.sonarsource.sonarlint.core.rpc.protocol.common.MQRModeDetails; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rpc.protocol.common.StandardModeDetails; import static org.sonarsource.sonarlint.core.tracking.TextRangeUtils.toTextRangeDto; public class RuleDetailsAdapter { public static final String INTRODUCTION_SECTION_KEY = "introduction"; public static final String ROOT_CAUSE_SECTION_KEY = "root_cause"; public static final String ASSESS_THE_PROBLEM_SECTION_KEY = "assess_the_problem"; public static final String HOW_TO_FIX_SECTION_KEY = "how_to_fix"; public static final String RESOURCES_SECTION_KEY = "resources"; private static final String DEFAULT_CONTEXT_KEY = "others"; private static final String DEFAULT_CONTEXT_DISPLAY_NAME = "Others"; private static final List SECTION_KEYS_ORDERED = List.of(ROOT_CAUSE_SECTION_KEY, ASSESS_THE_PROBLEM_SECTION_KEY, HOW_TO_FIX_SECTION_KEY, RESOURCES_SECTION_KEY); private RuleDetailsAdapter() { // utility class } public static EffectiveRuleDetailsDto transform(RuleDetails ruleDetails, @Nullable String contextKey) { var cleanCodeAttribute = ruleDetails.getCleanCodeAttribute().map(RuleDetailsAdapter::adapt).orElse(null); Either severityDetails = cleanCodeAttribute != null && !ruleDetails.getImpacts().isEmpty() ? Either.forRight(new MQRModeDetails(cleanCodeAttribute, toDto(ruleDetails.getImpacts()))) : Either.forLeft(new StandardModeDetails(adapt(Objects.requireNonNull(ruleDetails.getDefaultSeverity())), adapt(Objects.requireNonNull(ruleDetails.getType())))); return new EffectiveRuleDetailsDto( ruleDetails.getKey(), ruleDetails.getName(), severityDetails, transformDescriptions(ruleDetails, contextKey), transform(ruleDetails.getParams()), adapt(ruleDetails.getLanguage()), adapt(ruleDetails.getVulnerabilityProbability())); } public static Either transformDescriptions(RuleDetails ruleDetails, @Nullable String contextKey) { if (ruleDetails.hasMonolithicDescription()) { return Either.forLeft(transformMonolithicDescription(ruleDetails)); } return Either.forRight(transformSplitDescription(ruleDetails, contextKey)); } private static RuleMonolithicDescriptionDto transformMonolithicDescription(RuleDetails ruleDetails) { var htmlSnippets = new ArrayList(); if (!ruleDetails.getDescriptionSectionsByKey().isEmpty()) { // The rule has only `default` section htmlSnippets.addAll(ruleDetails.getDescriptionSectionsByKey().get("default").stream().map(RuleDetails.DescriptionSection::getHtmlContent).toList()); } else { htmlSnippets.add(ruleDetails.getHtmlDescription()); } htmlSnippets.add(ruleDetails.getExtendedDescription()); htmlSnippets.add(getCleanCodePrinciplesContent(ruleDetails.getCleanCodePrincipleKeys())); return new RuleMonolithicDescriptionDto(concat(htmlSnippets)); } private static String getCleanCodePrinciplesContent(Set cleanCodePrincipleKeys) { var principles = cleanCodePrincipleKeys.stream().sorted(Comparator.naturalOrder()).map(CleanCodePrinciples::getContent).toList(); return (principles.stream().anyMatch(StringUtils::isNotBlank) ? "

Clean Code Principles

\n" : "") + concat(principles); } private static RuleSplitDescriptionDto transformSplitDescription(RuleDetails ruleDetails, @Nullable String contextKey) { var sectionsByKey = ruleDetails.getDescriptionSectionsByKey(); var tabbedSections = new ArrayList<>(transformSectionsButIntroductionToTabs(ruleDetails, contextKey)); addMoreInfoTabIfNeeded(ruleDetails, tabbedSections); return new RuleSplitDescriptionDto(extractIntroductionFromSections(sectionsByKey), tabbedSections); } @Nullable private static String extractIntroductionFromSections(Map> sectionsByKey) { var introductionSections = sectionsByKey.get(INTRODUCTION_SECTION_KEY); String introductionHtmlContent = null; if (introductionSections != null && !introductionSections.isEmpty()) { // assume there is only one introduction section introductionHtmlContent = introductionSections.get(0).getHtmlContent(); } return introductionHtmlContent; } private static void addMoreInfoTabIfNeeded(RuleDetails ruleDetails, ArrayList tabbedSections) { if (!ruleDetails.getDescriptionSectionsByKey().containsKey(RESOURCES_SECTION_KEY)) { var htmlSnippets = new ArrayList(); htmlSnippets.add(ruleDetails.getExtendedDescription()); htmlSnippets.add(getCleanCodePrinciplesContent(ruleDetails.getCleanCodePrincipleKeys())); var content = concat(htmlSnippets); if (StringUtils.isNotBlank(content)) { tabbedSections .add(new RuleDescriptionTabDto(getTabTitle(ruleDetails, RESOURCES_SECTION_KEY), Either.forLeft(new RuleNonContextualSectionDto(content)))); } } } private static Collection transformSectionsButIntroductionToTabs(RuleDetails ruleDetails, @Nullable String contextKey) { var tabbedSections = new ArrayList(); var sectionsByKey = ruleDetails.getDescriptionSectionsByKey(); SECTION_KEYS_ORDERED.forEach(sectionKey -> { if (sectionsByKey.containsKey(sectionKey)) { var tabContents = sectionsByKey.get(sectionKey); Either content; var foundMatchingContext = tabContents.stream().anyMatch(c -> c.getContext().isPresent() && c.getContext().get().getKey().equals(contextKey)); if (tabContents.size() == 1 && tabContents.get(0).getContext().isEmpty()) { content = buildNonContextualSectionDto(ruleDetails, tabContents.get(0)); } else { // if there is more than one section, they should all have a context (verified in sonar-plugin-api) var contextualSectionContents = tabContents.stream().map(s -> { var context = s.getContext().get(); return new RuleContextualSectionDto(getTabContent(s, ruleDetails.getExtendedDescription(), ruleDetails.getCleanCodePrincipleKeys()), context.getKey(), context.getDisplayName()); }) .sorted(Comparator.comparing(RuleContextualSectionDto::getDisplayName)) .collect(Collectors.toCollection(ArrayList::new)); contextualSectionContents.add( new RuleContextualSectionDto(OthersSectionHtmlContent.getHtmlContent(), DEFAULT_CONTEXT_KEY, DEFAULT_CONTEXT_DISPLAY_NAME)); content = Either.forRight(new RuleContextualSectionWithDefaultContextKeyDto(foundMatchingContext ? contextKey : DEFAULT_CONTEXT_KEY, contextualSectionContents)); } tabbedSections.add(new RuleDescriptionTabDto(getTabTitle(ruleDetails, sectionKey), content)); } }); return tabbedSections; } private static String getTabTitle(RuleDetails ruleDetails, String sectionKey) { switch (sectionKey) { case ROOT_CAUSE_SECTION_KEY: return RuleType.SECURITY_HOTSPOT.equals(ruleDetails.getType()) ? "What's the risk?" : "Why is this an issue?"; case ASSESS_THE_PROBLEM_SECTION_KEY: return "Assess the risk"; case HOW_TO_FIX_SECTION_KEY: return "How can I fix it?"; default: return "More Info"; } } private static String concat(Collection htmlSnippets) { return htmlSnippets.stream() .filter(StringUtils::isNotBlank) .collect(Collectors.joining()); } private static String getTabContent(RuleDetails.DescriptionSection section, @Nullable String extendedDescription, Set educationPrincipleKeys) { var htmlSnippets = new ArrayList(); htmlSnippets.add(section.getHtmlContent()); if (RESOURCES_SECTION_KEY.equals(section.getKey())) { htmlSnippets.add(extendedDescription); htmlSnippets.add(getCleanCodePrinciplesContent(educationPrincipleKeys)); } return concat(htmlSnippets); } private static Collection transform(Collection params) { var builder = new ArrayList(); for (var param : params) { builder.add(new EffectiveRuleParamDto( param.getName(), param.getDescription(), param.getValue(), param.getDefaultValue())); } return builder; } @NotNull private static Either buildNonContextualSectionDto(RuleDetails ruleDetails, RuleDetails.DescriptionSection matchingContext) { return Either.forLeft(new RuleNonContextualSectionDto(getTabContent(matchingContext, ruleDetails.getExtendedDescription(), ruleDetails.getCleanCodePrincipleKeys()))); } public static List toDto(Map defaultImpacts) { return defaultImpacts.entrySet().stream() .map(e -> new ImpactDto(adapt(e.getKey()), adapt(e.getValue()))) .toList(); } public static CleanCodeAttribute adapt(org.sonarsource.sonarlint.core.commons.CleanCodeAttribute cca) { return CleanCodeAttribute.valueOf(cca.name()); } public static CleanCodeAttributeCategory adapt(org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory ccac) { return CleanCodeAttributeCategory.valueOf(ccac.name()); } public static IssueSeverity adapt(org.sonarsource.sonarlint.core.commons.IssueSeverity s) { return IssueSeverity.valueOf(s.name()); } public static org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType adapt(RuleType t) { return org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType.valueOf(t.name()); } public static Language adapt(SonarLanguage l) { return Language.valueOf(l.name()); } public static SoftwareQuality adapt(org.sonarsource.sonarlint.core.commons.SoftwareQuality sq) { return SoftwareQuality.valueOf(sq.name()); } @CheckForNull public static VulnerabilityProbability adapt(@Nullable org.sonarsource.sonarlint.core.commons.VulnerabilityProbability v) { return v != null ? VulnerabilityProbability.valueOf(v.name()) : null; } public static ImpactSeverity adapt(org.sonarsource.sonarlint.core.commons.ImpactSeverity is) { return ImpactSeverity.valueOf(is.name()); } public static IssueFlowDto adapt(Flow f) { return new IssueFlowDto(f.locations().stream().map(RuleDetailsAdapter::adapt).toList()); } public static IssueLocationDto adapt(org.sonarsource.sonarlint.core.analysis.api.IssueLocation l) { var inputFile = l.getInputFile(); var fileUri = inputFile != null ? inputFile.uri() : null; return new IssueLocationDto(toTextRangeDto(l.getTextRange()), l.getMessage(), fileUri); } public static QuickFixDto adapt(QuickFix qf) { List fileEditDto = qf.inputFileEdits().stream().map(RuleDetailsAdapter::adapt).toList(); return new QuickFixDto(fileEditDto, qf.message()); } private static FileEditDto adapt(ClientInputFileEdit edit) { return new FileEditDto(edit.target().uri(), edit.textEdits().stream().map(RuleDetailsAdapter::adapt).toList()); } private static TextEditDto adapt(TextEdit textEdit) { return new TextEditDto(Objects.requireNonNull(toTextRangeDto(textEdit.range())), textEdit.newText()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RuleNotFoundException.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; public class RuleNotFoundException extends Exception { private final String ruleKey; public RuleNotFoundException(String message, String ruleKey) { super(message); this.ruleKey = ruleKey; } public String getRuleKey() { return ruleKey; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesExtractionHelper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.util.List; import java.util.Map; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rule.extractor.RuleSettings; import org.sonarsource.sonarlint.core.rule.extractor.RulesDefinitionExtractor; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SECURITY_HOTSPOTS; public class RulesExtractionHelper { private final SonarLintLogger logger = SonarLintLogger.get(); private final PluginsService pluginsService; private final LanguageSupportRepository languageSupportRepository; private final RulesDefinitionExtractor ruleExtractor = new RulesDefinitionExtractor(); private final boolean enableSecurityHotspots; public RulesExtractionHelper(PluginsService pluginsService, LanguageSupportRepository languageSupportRepository, InitializeParams params) { this.pluginsService = pluginsService; this.languageSupportRepository = languageSupportRepository; this.enableSecurityHotspots = params.getBackendCapabilities().contains(SECURITY_HOTSPOTS); } public List extractEmbeddedRules() { logger.debug("Extracting standalone rules metadata"); return ruleExtractor.extractRules(pluginsService.getEmbeddedPlugins().plugins().getAllPluginInstancesByKeys(), languageSupportRepository.getEnabledLanguagesInStandaloneMode(), false, false, new RuleSettings(Map.of())); } public List extractRulesForConnection(String connectionId, Map globalSettings) { logger.debug("Extracting rules metadata for connection '{}'", connectionId); var settings = new RuleSettings(globalSettings); return ruleExtractor.extractRules(pluginsService.getPlugins(connectionId).plugins().getAllPluginInstancesByKeys(), languageSupportRepository.getEnabledLanguagesInConnectedMode(), true, enableSecurityHotspots, settings); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/RulesService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import jakarta.inject.Named; import jakarta.inject.Singleton; import java.util.Map; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleParamDefinitionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleParamType; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleParamDefinition; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleParamType; import static org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter.adapt; import static org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter.toDto; @Named @Singleton public class RulesService { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String IN_EMBEDDED_RULES = "' in embedded rules"; private final RulesRepository rulesRepository; public static final String COULD_NOT_FIND_RULE = "Could not find rule '"; public RulesService(RulesRepository rulesRepository) { this.rulesRepository = rulesRepository; } public Map listAllStandaloneRulesDefinitions() { return rulesRepository.getEmbeddedRules() .stream() .map(RulesService::convert) .collect(Collectors.toMap(RuleDefinitionDto::getKey, r -> r)); } @NotNull public static RuleDefinitionDto convert(SonarLintRuleDefinition r) { var cleanCodeAttribute = r.getCleanCodeAttribute().map(RuleDetailsAdapter::adapt).orElse(CleanCodeAttribute.CONVENTIONAL); return new RuleDefinitionDto(r.getKey(), r.getName(), cleanCodeAttribute, toDto(r.getDefaultImpacts()), convert(r.getParams()), r.isActiveByDefault(), adapt(r.getLanguage())); } private static Map convert(Map params) { return params.values().stream().map(RulesService::convert).collect(Collectors.toMap(RuleParamDefinitionDto::getKey, r -> r)); } private static RuleParamDefinitionDto convert(SonarLintRuleParamDefinition paramDef) { return new RuleParamDefinitionDto(paramDef.key(), paramDef.name(), paramDef.description(), paramDef.defaultValue(), convert(paramDef.type()), paramDef.multiple(), paramDef.possibleValues()); } private static RuleParamType convert(SonarLintRuleParamType type) { try { return RuleParamType.valueOf(type.name()); } catch (IllegalArgumentException unknownType) { LOG.warn("Unknown parameter type: {}", type.name()); return RuleParamType.STRING; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/rules/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.rules; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sca/DependencyRiskService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sca; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import javax.annotation.CheckForNull; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.DependencyRisksSynchronizedEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.CheckDependencyRiskSupportedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskTransition; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.OpenUrlInBrowserParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sca.DidChangeDependencyRisksParams; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.ScaSynchronizationService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.springframework.context.event.EventListener; public class DependencyRiskService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Version SCA_MIN_SQ_VERSION = Version.create("2025.4"); private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionRepository; private final StorageService storageService; private final SonarQubeClientManager sonarQubeClientManager; private final SonarProjectBranchTrackingService branchTrackingService; private final ScaSynchronizationService scaSynchronizationService; private final SonarLintRpcClient client; private final TelemetryService telemetryService; public DependencyRiskService(ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionRepository, StorageService storageService, SonarQubeClientManager sonarQubeClientManager, SonarProjectBranchTrackingService branchTrackingService, ScaSynchronizationService scaSynchronizationService, SonarLintRpcClient client, TelemetryService telemetryService) { this.configurationRepository = configurationRepository; this.connectionRepository = connectionRepository; this.storageService = storageService; this.sonarQubeClientManager = sonarQubeClientManager; this.branchTrackingService = branchTrackingService; this.scaSynchronizationService = scaSynchronizationService; this.client = client; this.telemetryService = telemetryService; } public List listAll(String configurationScopeId, boolean shouldRefresh, SonarLintCancelMonitor cancelMonitor) { return configurationRepository.getEffectiveBinding(configurationScopeId) .map(binding -> loadDependencyRisks(configurationScopeId, binding, shouldRefresh, cancelMonitor)) .orElseGet(Collections::emptyList); } @EventListener public void onDependencyRisksSynchronized(DependencyRisksSynchronizedEvent event) { var summary = event.summary(); var connectionId = event.connectionId(); var sonarProjectKey = event.sonarProjectKey(); configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, sonarProjectKey) .forEach(boundScope -> client.didChangeDependencyRisks(new DidChangeDependencyRisksParams(boundScope.getConfigScopeId(), summary.deletedItemIds(), summary.addedItems().stream() .map(DependencyRiskService::toDto) .toList(), summary.updatedItems().stream() .map(DependencyRiskService::toDto) .toList()))); } public CheckDependencyRiskSupportedResponse checkSupported(String configurationScopeId) { var configScope = configurationRepository.getConfigurationScope(configurationScopeId); if (configScope == null) { var error = new ResponseError(SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_FOUND, "The provided configuration scope does not exist: " + configurationScopeId, configurationScopeId); throw new ResponseErrorException(error); } var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); if (effectiveBinding.isEmpty()) { return new CheckDependencyRiskSupportedResponse(false, "The project is not bound, please bind it to SonarQube Server Enterprise 2025.4 or higher"); } var connectionId = effectiveBinding.get().connectionId(); var connection = connectionRepository.getConnectionById(connectionId); if (connection == null) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "The provided configuration scope is bound to an unknown connection: " + connectionId, connectionId); throw new ResponseErrorException(error); } var optServerInfo = storageService.connection(connectionId).serverInfo().read(); if (optServerInfo.isEmpty()) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Could not retrieve server information for connection", connectionId); throw new ResponseErrorException(error); } var serverInfo = optServerInfo.get(); if (!connection.getEndpointParams().isSonarCloud() && !serverInfo.version().satisfiesMinRequirement(SCA_MIN_SQ_VERSION)) { return new CheckDependencyRiskSupportedResponse(false, "The connected SonarQube Server version is lower than the minimum supported version 2025.4"); } if (!serverInfo.hasFeature(Feature.SCA)) { return new CheckDependencyRiskSupportedResponse(false, "The connected SonarQube Server does not have Advanced Security enabled (requires Enterprise edition or higher)"); } return new CheckDependencyRiskSupportedResponse(true, null); } private List loadDependencyRisks(String configurationScopeId, Binding binding, boolean shouldRefresh, SonarLintCancelMonitor cancelMonitor) { return branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId) .map(matchedBranch -> { if (shouldRefresh) { sonarQubeClientManager.withActiveClient(binding.connectionId(), serverApi -> scaSynchronizationService.synchronize(serverApi, binding.connectionId(), binding.sonarProjectKey(), matchedBranch, cancelMonitor)); } var projectStorage = storageService.binding(binding); return projectStorage.findings().loadDependencyRisks(matchedBranch) .stream().map(DependencyRiskService::toDto) .toList(); }).orElseGet(Collections::emptyList); } private static DependencyRiskDto toDto(ServerDependencyRisk serverDependencyRisk) { return new DependencyRiskDto( serverDependencyRisk.key(), DependencyRiskDto.Type.valueOf(serverDependencyRisk.type().name()), DependencyRiskDto.Severity.valueOf(serverDependencyRisk.severity().name()), DependencyRiskDto.SoftwareQuality.valueOf(serverDependencyRisk.quality().name()), DependencyRiskDto.Status.valueOf(serverDependencyRisk.status().name()), serverDependencyRisk.packageName(), serverDependencyRisk.packageVersion(), serverDependencyRisk.vulnerabilityId(), serverDependencyRisk.cvssScore(), serverDependencyRisk.transitions().stream() .map(transition -> DependencyRiskDto.Transition.valueOf(transition.name())) .toList()); } public void changeStatus(String configurationScopeId, UUID dependencyRiskKey, DependencyRiskTransition transition, @CheckForNull String comment, SonarLintCancelMonitor cancelMonitor) { var binding = configurationRepository.getEffectiveBindingOrThrow(configurationScopeId); var serverConnection = sonarQubeClientManager.getValidClientOrThrow(binding.connectionId()); var projectServerIssueStore = storageService.binding(binding).findings(); var branchName = branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId); if (branchName.isEmpty()) { throw new IllegalArgumentException("Could not determine matched branch for configuration scope " + configurationScopeId); } var dependencyRisks = projectServerIssueStore.loadDependencyRisks(branchName.get()); var dependencyRiskOpt = dependencyRisks.stream().filter(risk -> risk.key().equals(dependencyRiskKey)).findFirst(); if (dependencyRiskOpt.isEmpty()) { throw new DependencyRiskNotFoundException("Dependency Risk with key " + dependencyRiskKey + " was not found", dependencyRiskKey.toString()); } var dependencyRisk = dependencyRiskOpt.get(); if (!dependencyRisk.transitions().contains(adaptTransition(transition))) { throw new IllegalArgumentException("Transition " + transition + " is not allowed for this dependency risk"); } if ((transition == DependencyRiskTransition.ACCEPT || transition == DependencyRiskTransition.SAFE) && (comment == null || comment.isBlank())) { throw new IllegalArgumentException("Comment is required for ACCEPT and SAFE transitions"); } LOG.info("Changing status for dependency risk {} to {} with comment: {}", dependencyRiskKey, transition, comment); var newStatus = switch (transition) { case ACCEPT -> ServerDependencyRisk.Status.ACCEPT; case SAFE -> ServerDependencyRisk.Status.SAFE; case REOPEN -> ServerDependencyRisk.Status.OPEN; case CONFIRM -> ServerDependencyRisk.Status.CONFIRM; }; var updatedDependencyRisk = dependencyRisk.withStatus(newStatus); serverConnection.withClientApi(serverApi -> { serverApi.sca().changeStatus(dependencyRiskKey, transition.name(), comment, cancelMonitor); projectServerIssueStore.updateDependencyRiskStatus(dependencyRiskKey, newStatus, updatedDependencyRisk.transitions()); client.didChangeDependencyRisks(new DidChangeDependencyRisksParams(configurationScopeId, Set.of(), List.of(), List.of(toDto(updatedDependencyRisk)))); }); } private static ServerDependencyRisk.Transition adaptTransition(DependencyRiskTransition transition) { return switch (transition) { case REOPEN -> ServerDependencyRisk.Transition.REOPEN; case CONFIRM -> ServerDependencyRisk.Transition.CONFIRM; case ACCEPT -> ServerDependencyRisk.Transition.ACCEPT; case SAFE -> ServerDependencyRisk.Transition.SAFE; }; } public void openDependencyRiskInBrowser(String configurationScopeId, UUID dependencyKey) { var effectiveBinding = configurationRepository.getEffectiveBinding(configurationScopeId); var endpointParams = effectiveBinding.flatMap(binding -> connectionRepository.getEndpointParams(binding.connectionId())); if (effectiveBinding.isEmpty() || endpointParams.isEmpty()) { throw new IllegalArgumentException(String.format("Configuration scope '%s' is not bound properly, unable to open dependency risk", configurationScopeId)); } var branchName = branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId); if (branchName.isEmpty()) { throw new IllegalArgumentException(String.format("Configuration scope %s has no matching branch, unable to open dependency risk", configurationScopeId)); } var url = buildDependencyRiskBrowseUrl(effectiveBinding.get().sonarProjectKey(), branchName.get(), dependencyKey, endpointParams.get()); client.openUrlInBrowser(new OpenUrlInBrowserParams(url)); telemetryService.dependencyRiskInvestigatedRemotely(); } static String buildDependencyRiskBrowseUrl(String projectKey, String branch, UUID dependencyKey, EndpointParams endpointParams) { var relativePath = new StringBuilder("/dependency-risks/") .append(UrlUtils.urlEncode(dependencyKey.toString())) .append("/what?id=") .append(UrlUtils.urlEncode(projectKey)) .append("&branch=") .append(UrlUtils.urlEncode(branch)) .toString(); return ServerApiHelper.concat(endpointParams.getBaseUrl(), relativePath); } public static class DependencyRiskNotFoundException extends RuntimeException { private final String key; public DependencyRiskNotFoundException(String message, String key) { super(message); this.key = key; } public String getKey() { return key; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sca/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.sca; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/ServerEventsService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.server.event; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.event.ConnectionCredentialsChangedEvent; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SERVER_SENT_EVENTS; public class ServerEventsService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final SonarQubeClientManager sonarQubeClientManager; private final LanguageSupportRepository languageSupportRepository; private final boolean shouldManageServerSentEvents; private final ApplicationEventPublisher eventPublisher; private final Map streamsPerConnectionId = new ConcurrentHashMap<>(); private final ExecutorService executorService = FailSafeExecutors.newSingleThreadExecutor("sonarlint-server-sent-events-subscriber"); public ServerEventsService(ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionConfigurationRepository, SonarQubeClientManager sonarQubeClientManager, LanguageSupportRepository languageSupportRepository, InitializeParams initializeParams, ApplicationEventPublisher eventPublisher) { this.configurationRepository = configurationRepository; this.connectionConfigurationRepository = connectionConfigurationRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.languageSupportRepository = languageSupportRepository; this.shouldManageServerSentEvents = initializeParams.getBackendCapabilities().contains(SERVER_SENT_EVENTS); this.eventPublisher = eventPublisher; } @EventListener public void handle(ConfigurationScopesAddedWithBindingEvent event) { if (!shouldManageServerSentEvents) { return; } executorService.execute(() -> subscribeAll(event.getConfigScopeIds())); } @EventListener public void handle(ConfigurationScopeRemovedEvent event) { if (!shouldManageServerSentEvents) { return; } var removedScope = event.removedConfigurationScope(); var removedBindingConfiguration = event.removedBindingConfiguration(); var bindingConfigurationFromRepository = configurationRepository.getBindingConfiguration(removedScope.id()); if (bindingConfigurationFromRepository == null || isBindingDifferent(removedBindingConfiguration, bindingConfigurationFromRepository)) { // it has not been re-added in the meantime, or re-added with different binding executorService.execute(() -> unsubscribe(removedBindingConfiguration)); } } @EventListener public void handle(BindingConfigChangedEvent event) { if (!shouldManageServerSentEvents) { return; } var previousBinding = event.previousConfig(); if (isBindingDifferent(previousBinding, event.newConfig())) { executorService.execute(() -> { unsubscribe(previousBinding); subscribe(event.configScopeId()); }); } } @EventListener public void handle(ConnectionConfigurationAddedEvent event) { if (!shouldManageServerSentEvents) { return; } // This is only to handle the case where binding was invalid (connection did not exist) and became valid (matching connection was created) var connectionId = event.addedConnectionId(); executorService.execute(() -> subscribe(connectionId, configurationRepository.getSonarProjectsUsedForConnection(connectionId))); } @EventListener public void handle(ConnectionConfigurationRemovedEvent event) { if (!shouldManageServerSentEvents) { return; } executorService.execute(() -> { var stream = streamsPerConnectionId.remove(event.removedConnectionId()); if (stream != null) { stream.stop(); } }); } @EventListener public void handle(ConnectionConfigurationUpdatedEvent event) { if (!shouldManageServerSentEvents) { return; } // URL might have changed, in doubt resubscribe executorService.execute(() -> resubscribe(event.updatedConnectionId())); } @EventListener public void handle(ConnectionCredentialsChangedEvent event) { if (!shouldManageServerSentEvents) { return; } executorService.execute(() -> resubscribe(event.getConnectionId())); } private static boolean isBindingDifferent(BindingConfiguration previousConfig, BindingConfiguration newConfig) { return !Objects.equals(previousConfig.sonarProjectKey(), newConfig.sonarProjectKey()) || !Objects.equals(previousConfig.connectionId(), newConfig.connectionId()); } private void subscribeAll(Set configurationScopeIds) { configurationScopeIds.stream() .map(configurationRepository::getConfiguredBinding) .flatMap(Optional::stream) .collect(Collectors.groupingBy(Binding::connectionId, mapping(Binding::sonarProjectKey, toSet()))) .forEach(this::subscribe); } private void subscribe(String scopeId) { configurationRepository.getConfiguredBinding(scopeId) .ifPresent(binding -> subscribe(binding.connectionId(), Set.of(binding.sonarProjectKey()))); } private void subscribe(String connectionId, Set possiblyNewProjectKeys) { if (supportsServerSentEvents(connectionId)) { var stream = streamsPerConnectionId.computeIfAbsent(connectionId, k -> openStream(connectionId)); stream.subscribeNew(possiblyNewProjectKeys); } } private SonarQubeEventStream openStream(String connectionId) { return new SonarQubeEventStream(languageSupportRepository.getEnabledLanguagesInConnectedMode(), connectionId, sonarQubeClientManager, e -> eventPublisher.publishEvent(new SonarServerEventReceivedEvent(connectionId, e))); } private boolean supportsServerSentEvents(String connectionId) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); return connection != null && connection.getKind() == ConnectionKind.SONARQUBE; } private void unsubscribe(BindingConfiguration previousBindingConfiguration) { if (previousBindingConfiguration.isBound()) { var connectionId = requireNonNull(previousBindingConfiguration.connectionId()); var projectKey = requireNonNull(previousBindingConfiguration.sonarProjectKey()); if (supportsServerSentEvents(connectionId) && streamsPerConnectionId.containsKey(connectionId) && configurationRepository.getSonarProjectsUsedForConnection(connectionId).stream().noneMatch(usedProjectKey -> usedProjectKey.equals(projectKey))) { streamsPerConnectionId.get(connectionId).unsubscribe(projectKey); } } } private void resubscribe(String connectionId) { if (supportsServerSentEvents(connectionId) && streamsPerConnectionId.containsKey(connectionId)) { streamsPerConnectionId.get(connectionId).resubscribe(); } } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop server-sent events subscriber service in a timely manner"); } streamsPerConnectionId.values().forEach(SonarQubeEventStream::stop); streamsPerConnectionId.clear(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/SonarQubeEventStream.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.server.event; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Consumer; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; import org.sonarsource.sonarlint.core.serverapi.stream.EventStream; public class SonarQubeEventStream { private static final SonarLintLogger LOG = SonarLintLogger.get(); private EventStream eventStream; private final Set subscribedProjectKeys = new LinkedHashSet<>(); private final Set enabledLanguages; private final String connectionId; private final SonarQubeClientManager sonarQubeClientManager; private final Consumer eventConsumer; public SonarQubeEventStream(Set enabledLanguages, String connectionId, SonarQubeClientManager sonarQubeClientManager, Consumer eventConsumer) { this.enabledLanguages = enabledLanguages; this.connectionId = connectionId; this.sonarQubeClientManager = sonarQubeClientManager; this.eventConsumer = eventConsumer; } public synchronized void subscribeNew(Set possiblyNewProjectKeys) { if (!possiblyNewProjectKeys.isEmpty() && !subscribedProjectKeys.containsAll(possiblyNewProjectKeys)) { cancelSubscription(); subscribedProjectKeys.addAll(possiblyNewProjectKeys); attemptSubscription(subscribedProjectKeys); } } public synchronized void resubscribe() { cancelSubscription(); if (!subscribedProjectKeys.isEmpty()) { attemptSubscription(subscribedProjectKeys); } } public synchronized void unsubscribe(String projectKey) { cancelSubscription(); subscribedProjectKeys.remove(projectKey); if (!subscribedProjectKeys.isEmpty()) { attemptSubscription(subscribedProjectKeys); } } private void attemptSubscription(Set projectKeys) { if (!enabledLanguages.isEmpty()) { try { sonarQubeClientManager.withActiveClient(connectionId, serverApi -> eventStream = serverApi.push().subscribe(projectKeys, enabledLanguages, e -> notifyHandlers(e, eventConsumer))); } catch (Exception e) { LOG.debug("Error while subscribing to event-stream", e); } } } private static void notifyHandlers(SonarServerEvent sonarServerEvent, Consumer clientEventConsumer) { clientEventConsumer.accept(sonarServerEvent); } private void cancelSubscription() { if (eventStream != null) { eventStream.close(); eventStream = null; } } public synchronized void stop() { subscribedProjectKeys.clear(); cancelSubscription(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/server/event/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.server.event; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/LastEventPolling.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.smartnotifications; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import org.sonarsource.sonarlint.core.storage.StorageService; public class LastEventPolling { private final StorageService storage; public LastEventPolling(StorageService storage) { this.storage = storage; } public ZonedDateTime getLastEventPolling(String connectionId, String projectKey) { var lastEventPollingEpoch = storage.connection(connectionId).project(projectKey).smartNotifications().readLastEventPolling(); return lastEventPollingEpoch.map(aLong -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(aLong), ZoneId.systemDefault())) .orElseGet(ZonedDateTime::now); } public void setLastEventPolling(ZonedDateTime dateTime, String connectionId, String projectKey) { var smartNotificationsStorage = storage.connection(connectionId).project(projectKey).smartNotifications(); var lastEventPolling = smartNotificationsStorage.readLastEventPolling(); var dateTimeEpoch = dateTime.toInstant().toEpochMilli(); if (lastEventPolling.isPresent() && dateTimeEpoch <= lastEventPolling.get()) { // this can happen if the settings changed between the read and write return; } smartNotificationsStorage.store(dateTimeEpoch); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/SmartNotifications.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.smartnotifications; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.AbstractConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.developers.SearchEventsResponseDto; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.sonarsource.sonarlint.core.websocket.WebSocketService; import org.sonarsource.sonarlint.core.websocket.events.SmartNotificationEvent; import org.springframework.context.event.EventListener; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SMART_NOTIFICATIONS; public class SmartNotifications { private final SonarLintLogger logger = SonarLintLogger.get(); private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionRepository; private final SonarQubeClientManager sonarQubeClientManager; private final SonarLintRpcClient client; private final TelemetryService telemetryService; private final WebSocketService webSocketService; private final InitializeParams params; private final LastEventPolling lastEventPollingService; private ExecutorServiceShutdownWatchable smartNotificationsPolling; public SmartNotifications(ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionRepository, SonarQubeClientManager sonarQubeClientManager, SonarLintRpcClient client, StorageService storageService, TelemetryService telemetryService, WebSocketService webSocketService, InitializeParams params) { this.configurationRepository = configurationRepository; this.connectionRepository = connectionRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.client = client; this.telemetryService = telemetryService; this.webSocketService = webSocketService; this.params = params; lastEventPollingService = new LastEventPolling(storageService); } @PostConstruct public void initialize() { if (!params.getBackendCapabilities().contains(SMART_NOTIFICATIONS)) { return; } smartNotificationsPolling = new ExecutorServiceShutdownWatchable<>(FailSafeExecutors.newSingleThreadScheduledExecutor("Smart Notifications Polling")); var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(smartNotificationsPolling); smartNotificationsPolling.getWrapped().scheduleAtFixedRate(() -> this.poll(cancelMonitor), 1, 60, TimeUnit.SECONDS); } private void poll(SonarLintCancelMonitor cancelMonitor) { var boundScopeByConnectionAndSonarProject = configurationRepository.getBoundScopeByConnectionAndSonarProject(); boundScopeByConnectionAndSonarProject.forEach((connectionId, boundScopesByProject) -> { var connection = connectionRepository.getConnectionById(connectionId); if (connection != null && !connection.isDisableNotifications() && !shouldSkipPolling(connection)) { sonarQubeClientManager.withActiveClient(connectionId, serverApi -> manageNotificationsForConnection(serverApi, boundScopesByProject, connection, cancelMonitor)); } }); } private void manageNotificationsForConnection(ServerApi serverApi, Map> boundScopesPerProjectKey, AbstractConnectionConfiguration connection, SonarLintCancelMonitor cancelMonitor) { var developersApi = serverApi.developers(); var connectionId = connection.getConnectionId(); var projectKeysByLastEventPolling = boundScopesPerProjectKey.keySet().stream() .collect(Collectors.toMap(Function.identity(), p -> getLastNotificationTime(lastEventPollingService.getLastEventPolling(connectionId, p)))); List notifications; try { notifications = developersApi.searchEvents(projectKeysByLastEventPolling, cancelMonitor).events(); } catch (Exception e) { logger.error("Failed to get notifications", e); notifications = List.of(); } for (var n : notifications) { var scopeIds = boundScopesPerProjectKey.get(n.project()).stream().map(BoundScope::getConfigScopeId).collect(Collectors.toSet()); var smartNotification = new ShowSmartNotificationParams(n.message(), n.link(), scopeIds, n.category(), connectionId); client.showSmartNotification(smartNotification); telemetryService.smartNotificationsReceived(n.category()); } projectKeysByLastEventPolling.keySet() .forEach(projectKey -> lastEventPollingService.setLastEventPolling(ZonedDateTime.now(), connectionId, projectKey)); } private boolean shouldSkipPolling(AbstractConnectionConfiguration connection) { if (connection.getKind() == ConnectionKind.SONARCLOUD) { var region = ((SonarCloudConnectionConfiguration) connection).getRegion(); return webSocketService.hasOpenConnection(region); } return false; } @PreDestroy public void shutdown() { if (smartNotificationsPolling != null && !MoreExecutors.shutdownAndAwaitTermination(smartNotificationsPolling, 5, TimeUnit.SECONDS)) { logger.warn("Unable to stop smart notifications executor service in a timely manner"); } } private static ZonedDateTime getLastNotificationTime(ZonedDateTime lastTime) { var oneDayAgo = ZonedDateTime.now().minusDays(1); return lastTime.isAfter(oneDayAgo) ? lastTime : oneDayAgo; } @EventListener public void onServerEventReceived(SonarServerEventReceivedEvent eventReceived) { var serverEvent = eventReceived.getEvent(); if (serverEvent instanceof SmartNotificationEvent smartNotificationEvent) { notifyClient(eventReceived.getConnectionId(), smartNotificationEvent); } } private void notifyClient(String connectionId, SmartNotificationEvent event) { var projectKey = event.project(); var boundScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); client.showSmartNotification(new ShowSmartNotificationParams(event.message(), event.link(), boundScopes.stream().map(BoundScope::getConfigScopeId).collect(Collectors.toSet()), event.category(), connectionId)); telemetryService.smartNotificationsReceived(event.category()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/smartnotifications/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.smartnotifications; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SonarLintSpringAppConfig.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.spring; import java.net.ProxySelector; import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.ExecutorService; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.core5.util.Timeout; import org.jooq.DSLContext; import org.sonarsource.sonarlint.core.BindingCandidatesFinder; import org.sonarsource.sonarlint.core.BindingClueProvider; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.ConfigurationService; import org.sonarsource.sonarlint.core.ConnectionService; import org.sonarsource.sonarlint.core.ConnectionSuggestionProvider; import org.sonarsource.sonarlint.core.MCPServerConfigurationProvider; import org.sonarsource.sonarlint.core.OrganizationsCache; import org.sonarsource.sonarlint.core.SharedConnectedModeSettingsProvider; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCodeContextService; import org.sonarsource.sonarlint.core.SonarProjectsCache; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.TokenGeneratorHelper; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.VersionSoonUnsupportedHelper; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.ai.ide.AiAgentService; import org.sonarsource.sonarlint.core.ai.ide.AiHookService; import org.sonarsource.sonarlint.core.analysis.AnalysisSchedulerCache; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.analysis.UserAnalysisPropertiesRepository; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.dogfood.DogfoodEnvironmentDetectionService; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.embedded.server.AnalyzeFileListRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.AwaitingUserTokenFutureRepository; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.embedded.server.ToggleAutomaticAnalysisRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.GeneratedUserTokenHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowFixSuggestionRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowHotspotRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.ShowIssueRequestHandler; import org.sonarsource.sonarlint.core.embedded.server.handler.StatusRequestHandler; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.file.ServerFilePathsProvider; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.fs.FileExclusionService; import org.sonarsource.sonarlint.core.fs.OpenFilesRepository; import org.sonarsource.sonarlint.core.hotspot.HotspotService; import org.sonarsource.sonarlint.core.http.AskClientCertificatePredicate; import org.sonarsource.sonarlint.core.http.ClientProxyCredentialsProvider; import org.sonarsource.sonarlint.core.http.ClientProxySelector; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.http.HttpConfig; import org.sonarsource.sonarlint.core.http.ThreadFactories; import org.sonarsource.sonarlint.core.http.ssl.CertificateStore; import org.sonarsource.sonarlint.core.http.ssl.SslConfig; import org.sonarsource.sonarlint.core.issue.IssueService; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.local.only.XodusLocalOnlyIssueStorageService; import org.sonarsource.sonarlint.core.log.LogService; import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.monitoring.MonitoringInitializationParams; import org.sonarsource.sonarlint.core.monitoring.MonitoringService; import org.sonarsource.sonarlint.core.monitoring.MonitoringUserIdStore; import org.sonarsource.sonarlint.core.newcode.NewCodeService; import org.sonarsource.sonarlint.core.plugin.PluginLifecycleService; import org.sonarsource.sonarlint.core.plugin.PluginStatusNotifierService; import org.sonarsource.sonarlint.core.plugin.PluginsRepository; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ConnectedArtifactsLoadingStrategyFactory; import org.sonarsource.sonarlint.core.plugin.loading.strategy.StandaloneArtifactsLoadingStrategy; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsNotifierService; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsRepository; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesLocalCacheManager; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesSignatureVerifier; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginDownloader; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginsCache; import org.sonarsource.sonarlint.core.progress.ClientAwareTaskManager; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.reporting.PreviouslyRaisedFindingsRepository; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.HttpConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SslConfigurationDto; import org.sonarsource.sonarlint.core.rules.RulesExtractionHelper; import org.sonarsource.sonarlint.core.rules.RulesService; import org.sonarsource.sonarlint.core.sca.DependencyRiskService; import org.sonarsource.sonarlint.core.server.event.ServerEventsService; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.KnownFindingsRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.LocalOnlyIssuesRepository; import org.sonarsource.sonarlint.core.smartnotifications.SmartNotifications; import org.sonarsource.sonarlint.core.storage.SonarLintDatabaseService; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.FindingsSynchronizationService; import org.sonarsource.sonarlint.core.sync.HotspotSynchronizationService; import org.sonarsource.sonarlint.core.sync.IssueSynchronizationService; import org.sonarsource.sonarlint.core.sync.ScaSynchronizationService; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesSynchronizationService; import org.sonarsource.sonarlint.core.sync.SynchronizationService; import org.sonarsource.sonarlint.core.sync.TaintSynchronizationService; import org.sonarsource.sonarlint.core.telemetry.TelemetryLocalStorageManager; import org.sonarsource.sonarlint.core.tracking.LocalOnlyIssueRepository; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import org.sonarsource.sonarlint.core.tracking.TrackingService; import org.sonarsource.sonarlint.core.tracking.XodusKnownFindingsStorageService; import org.sonarsource.sonarlint.core.websocket.WebSocketService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.scheduling.support.TaskUtils; import static org.sonarsource.sonarlint.core.http.ssl.CertificateStore.DEFAULT_PASSWORD; import static org.sonarsource.sonarlint.core.http.ssl.CertificateStore.DEFAULT_STORE_TYPE; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.MONITORING; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.TELEMETRY; @Configuration // Can't use classpath scanning in OSGi, so waiting to move out of process, we have to declare our beans manually // @ComponentScan(basePackages = "org.sonarsource.sonarlint.core") @Import({ AskClientCertificatePredicate.class, ClientProxySelector.class, ClientProxyCredentialsProvider.class, ConfigurationService.class, ConfigurationRepository.class, RulesService.class, SonarQubeClientManager.class, ConnectionConfigurationRepository.class, RulesRepository.class, RulesExtractionHelper.class, PluginsService.class, SkippedPluginsNotifierService.class, PluginStatusNotifierService.class, PluginsRepository.class, PluginLifecycleService.class, SkippedPluginsRepository.class, LanguageSupportRepository.class, ConnectionService.class, TokenGeneratorHelper.class, EmbeddedServer.class, StatusRequestHandler.class, GeneratedUserTokenHandler.class, AwaitingUserTokenFutureRepository.class, ShowHotspotRequestHandler.class, ShowIssueRequestHandler.class, ShowFixSuggestionRequestHandler.class, BindingSuggestionProvider.class, ConnectionSuggestionProvider.class, BindingClueProvider.class, SonarProjectsCache.class, SonarProjectBranchTrackingService.class, SynchronizationService.class, HotspotService.class, IssueService.class, AnalysisService.class, SmartNotifications.class, LocalOnlyIssueRepository.class, WebSocketService.class, ServerEventsService.class, VersionSoonUnsupportedHelper.class, XodusLocalOnlyIssueStorageService.class, StorageService.class, SeverityModeService.class, NewCodeService.class, RequestHandlerBindingAssistant.class, TaintVulnerabilityTrackingService.class, SonarProjectBranchesSynchronizationService.class, TaintSynchronizationService.class, IssueSynchronizationService.class, HotspotSynchronizationService.class, ClientFileSystemService.class, SonarCodeContextService.class, PathTranslationService.class, ServerFilePathsProvider.class, FileExclusionService.class, NodeJsService.class, OrganizationsCache.class, BindingCandidatesFinder.class, SharedConnectedModeSettingsProvider.class, MCPServerConfigurationProvider.class, AnalysisSchedulerCache.class, XodusKnownFindingsStorageService.class, TrackingService.class, FindingsSynchronizationService.class, FindingReportingService.class, PreviouslyRaisedFindingsRepository.class, UserAnalysisPropertiesRepository.class, OpenFilesRepository.class, DogfoodEnvironmentDetectionService.class, MonitoringService.class, MonitoringUserIdStore.class, AiCodeFixService.class, ClientAwareTaskManager.class, ScaSynchronizationService.class, DependencyRiskService.class, ToggleAutomaticAnalysisRequestHandler.class, AnalyzeFileListRequestHandler.class, AiAgentService.class, AiHookService.class, LogService.class, ActiveRulesService.class, AiCodeFixRepository.class, SonarLintDatabaseService.class, LocalOnlyIssuesRepository.class, ServerPluginsCache.class, KnownFindingsRepository.class, StandaloneArtifactsLoadingStrategy.class, ConnectedArtifactsLoadingStrategyFactory.class, BinariesArtifactSource.class, BinariesLocalCacheManager.class, BinariesSignatureVerifier.class, ServerPluginDownloader.class }) public class SonarLintSpringAppConfig { @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { var eventMulticaster = new SimpleApplicationEventMulticaster(); eventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER); return eventMulticaster; } @Bean UserPaths provideClientPaths(InitializeParams initializeParams) { return UserPaths.from(initializeParams); } @Bean SonarCloudActiveEnvironment provideSonarCloudActiveEnvironment(InitializeParams params) { var alternativeSonarCloudEnv = params.getAlternativeSonarCloudEnvironment(); return alternativeSonarCloudEnv == null ? SonarCloudActiveEnvironment.prod() : new SonarCloudActiveEnvironment(alternativeSonarCloudEnv.getAlternateRegionUris()); } @Bean HttpClientProvider provideHttpClientProvider(InitializeParams params, UserPaths userPaths, AskClientCertificatePredicate askClientCertificatePredicate, ProxySelector proxySelector, CredentialsProvider proxyCredentialsProvider) { return new HttpClientProvider(params.getClientConstantInfo().getUserAgent(), adapt(params.getHttpConfiguration(), userPaths.getUserHome()), askClientCertificatePredicate, proxySelector, proxyCredentialsProvider); } @Bean MonitoringInitializationParams provideMonitoringInitParams(InitializeParams params, TelemetryLocalStorageManager telemetryService) { return new MonitoringInitializationParams( params.getBackendCapabilities().contains(MONITORING), params.getBackendCapabilities().contains(TELEMETRY) && telemetryService.isEnabled(), params.getTelemetryConstantAttributes().getProductKey(), params.getTelemetryConstantAttributes().getProductVersion(), params.getTelemetryConstantAttributes().getIdeVersion()); } // disable automatic destroy call, shutdown is handled by SonarLintDatabaseService // MonitoringService dependency ensures Sentry is initialized before database migrations run @Bean(destroyMethod = "") SonarLintDatabase provideDatabase(UserPaths userPaths, MonitoringService monitoringService) { return new SonarLintDatabase(userPaths.getStorageRoot()); } @Bean DSLContext provideDSLContext(SonarLintDatabase database) { return database.dsl(); } private static HttpConfig adapt(HttpConfigurationDto dto, @Nullable Path sonarlintUserHome) { return new HttpConfig(adapt(dto.getSslConfiguration(), sonarlintUserHome), toTimeout(dto.getConnectTimeout()), toTimeout(dto.getSocketTimeout()), toTimeout(dto.getConnectionRequestTimeout()), toTimeout(dto.getResponseTimeout())); } private static SslConfig adapt(SslConfigurationDto dto, @Nullable Path sonarlintUserHome) { return new SslConfig( adaptStore(dto.getKeyStorePath(), dto.getKeyStorePassword(), dto.getKeyStoreType(), sonarlintUserHome, "keystore"), adaptStore(dto.getTrustStorePath(), dto.getTrustStorePassword(), dto.getTrustStoreType(), sonarlintUserHome, "truststore")); } private static CertificateStore adaptStore(@Nullable Path storePathConfig, @Nullable String storePasswordConfig, @Nullable String storeTypeConfig, @Nullable Path sonarlintUserHome, String defaultStoreName) { var storePath = storePathConfig; if (storePath == null && sonarlintUserHome != null) { storePath = sonarlintUserHome.resolve("ssl/" + defaultStoreName + ".p12"); } if (storePath != null) { var keyStorePassword = storePasswordConfig == null ? DEFAULT_PASSWORD : storePasswordConfig; var keyStoreType = storeTypeConfig == null ? DEFAULT_STORE_TYPE : storeTypeConfig; return new CertificateStore(storePath, keyStorePassword, keyStoreType); } return null; } @CheckForNull private static Timeout toTimeout(@Nullable Duration duration) { return duration == null ? null : Timeout.of(duration); } @Bean(name = "pluginDownloadExecutor", destroyMethod = "shutdown") public ExecutorService pluginDownloadExecutor() { return FailSafeExecutors.newCachedThreadPool(ThreadFactories.threadWithNamePrefix("sonarlint-plugin-download-")); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/SpringApplicationContextInitializer.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.spring; import org.sonarsource.sonarlint.core.labs.IdeLabsSpringConfig; import org.sonarsource.sonarlint.core.promotion.PromotionSpringConfig; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.telemetry.TelemetrySpringConfig; import org.sonarsource.sonarlint.core.telemetry.gessie.GessieSpringConfig; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static java.util.Objects.requireNonNull; public class SpringApplicationContextInitializer implements AutoCloseable { private final AnnotationConfigApplicationContext applicationContext; public SpringApplicationContextInitializer(SonarLintRpcClient client, InitializeParams params) { applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(SonarLintSpringAppConfig.class); applicationContext.register(TelemetrySpringConfig.class); applicationContext.register(GessieSpringConfig.class); applicationContext.register(IdeLabsSpringConfig.class); applicationContext.register(PromotionSpringConfig.class); applicationContext.registerBean("sonarlintClient", SonarLintRpcClient.class, () -> requireNonNull(client)); applicationContext.registerBean("initializeParams", InitializeParams.class, () -> params); applicationContext.refresh(); } public ConfigurableApplicationContext getInitializedApplicationContext() { return applicationContext; } @Override public void close() throws Exception { applicationContext.close(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/spring/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.spring; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/SonarLintDatabaseService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.storage; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.LocalOnlyIssuesRepository; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_BRANCHES; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_DEPENDENCY_RISKS; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_FINDINGS; @Component @Lazy(false) public class SonarLintDatabaseService { private final SonarLintDatabase database; private final LocalOnlyIssuesRepository localOnlyIssuesRepository; private final AiCodeFixRepository aiCodeFixRepository; private final Set initialConnectionIds; public SonarLintDatabaseService(SonarLintDatabase database, LocalOnlyIssuesRepository localOnlyIssuesRepository, AiCodeFixRepository aiCodeFixRepository, InitializeParams params) { this.database = database; this.localOnlyIssuesRepository = localOnlyIssuesRepository; this.aiCodeFixRepository = aiCodeFixRepository; this.initialConnectionIds = Stream.concat( params.getSonarQubeConnections().stream().map(SonarQubeConnectionConfigurationDto::getConnectionId), params.getSonarCloudConnections().stream().map(SonarCloudConnectionConfigurationDto::getConnectionId)) .collect(Collectors.toSet()); } public SonarLintDatabase getDatabase() { return database; } @PostConstruct public void postConstruct() { cleanupNonExistingConnections(); localOnlyIssuesRepository.purgeIssuesOlderThan(Instant.now().minus(7, ChronoUnit.DAYS)); } private void cleanupNonExistingConnections() { aiCodeFixRepository.deleteUnknownConnections(initialConnectionIds); // this should be moved to ServerFindingRepository but the current design does not allow it database.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.CONNECTION_ID.notIn(initialConnectionIds)) .execute(); database.dsl().deleteFrom(SERVER_DEPENDENCY_RISKS) .where(SERVER_DEPENDENCY_RISKS.CONNECTION_ID.notIn(initialConnectionIds)) .execute(); database.dsl().deleteFrom(SERVER_BRANCHES) .where(SERVER_BRANCHES.CONNECTION_ID.notIn(initialConnectionIds)) .execute(); } @PreDestroy public void preDestroy() { database.shutdown(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/StorageService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.storage; import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.springframework.context.event.EventListener; public class StorageService { private final Path globalStorageRoot; private final Map connectionStorageById = new ConcurrentHashMap<>(); private final SonarLintDatabaseService databaseService; public StorageService(UserPaths userPaths, SonarLintDatabaseService databaseService) { this.globalStorageRoot = userPaths.getStorageRoot(); this.databaseService = databaseService; } public ConnectionStorage connection(String connectionId) { return connectionStorageById.computeIfAbsent(connectionId, k -> new ConnectionStorage(globalStorageRoot, connectionId, databaseService.getDatabase())); } public SonarProjectStorage binding(Binding binding) { return connection(binding.connectionId()).project(binding.sonarProjectKey()); } @EventListener public void handleEvent(ConnectionConfigurationRemovedEvent connectionConfigurationRemovedEvent) { var removedConnectionId = connectionConfigurationRemovedEvent.removedConnectionId(); var connectionStorage = connection(removedConnectionId); connectionStorage.delete(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/storage/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.storage; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/AnalyzerConfigurationSynchronized.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.util.Set; import org.sonarsource.sonarlint.core.commons.Binding; public record AnalyzerConfigurationSynchronized(Binding binding, Set configScopeIds) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/BranchBinding.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.util.Objects; import org.sonarsource.sonarlint.core.commons.Binding; public class BranchBinding { private final Binding binding; private final String branchName; public BranchBinding(Binding binding, String branchName) { this.binding = binding; this.branchName = branchName; } public Binding getBinding() { return binding; } public String getBranchName() { return branchName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BranchBinding that = (BranchBinding) o; return Objects.equals(binding, that.binding) && Objects.equals(branchName, that.branchName); } @Override public int hashCode() { return Objects.hash(binding, branchName); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/ConfigurationScopesSynchronizedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.util.Set; public class ConfigurationScopesSynchronizedEvent { private final Set configScopeIds; public ConfigurationScopesSynchronizedEvent(Set configScopeIds) { this.configScopeIds = configScopeIds; } public Set getConfigScopeIds() { return configScopeIds; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/FindingsSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.nio.file.Path; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; public class FindingsSynchronizationService { private static final int FETCH_ALL_ISSUES_THRESHOLD = 10; private final ConfigurationRepository configurationRepository; private final SonarProjectBranchTrackingService branchTrackingService; private final PathTranslationService pathTranslationService; private final IssueSynchronizationService issueSynchronizationService; private final HotspotSynchronizationService hotspotSynchronizationService; private final ExecutorService issueUpdaterExecutorService; private final boolean shouldRefreshHotspots; public FindingsSynchronizationService(ConfigurationRepository configurationRepository, SonarProjectBranchTrackingService branchTrackingService, PathTranslationService pathTranslationService, IssueSynchronizationService issueSynchronizationService, HotspotSynchronizationService hotspotSynchronizationService, InitializeParams initializeParams) { this.configurationRepository = configurationRepository; this.branchTrackingService = branchTrackingService; this.pathTranslationService = pathTranslationService; this.issueSynchronizationService = issueSynchronizationService; this.hotspotSynchronizationService = hotspotSynchronizationService; this.issueUpdaterExecutorService = FailSafeExecutors.newSingleThreadExecutor("sonarlint-server-tracking-issue-updater"); this.shouldRefreshHotspots = initializeParams.getBackendCapabilities().contains(BackendCapability.SECURITY_HOTSPOTS); } public void refreshServerFindings(String configurationScopeId, Set pathsToRefresh) { var effectiveBindingOpt = configurationRepository.getEffectiveBinding(configurationScopeId); var activeBranchOpt = branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId); var translationOpt = pathTranslationService.getOrComputePathTranslation(configurationScopeId); if (effectiveBindingOpt.isPresent() && activeBranchOpt.isPresent() && translationOpt.isPresent()) { var binding = effectiveBindingOpt.get(); var activeBranch = activeBranchOpt.get(); var translation = translationOpt.get(); var cancelMonitor = new SonarLintCancelMonitor(); refreshServerIssues(cancelMonitor, binding, activeBranch, pathsToRefresh, translation); if (shouldRefreshHotspots) { refreshServerSecurityHotspots(cancelMonitor, binding, activeBranch, pathsToRefresh, translationOpt.get()); } } } private void refreshServerIssues(SonarLintCancelMonitor cancelMonitor, Binding binding, String activeBranch, Set pathsInvolved, FilePathTranslation translation) { var serverFileRelativePaths = pathsInvolved.stream().map(translation::ideToServerPath).collect(Collectors.toSet()); var downloadAllIssuesAtOnce = serverFileRelativePaths.size() > FETCH_ALL_ISSUES_THRESHOLD; var fetchTasks = new LinkedList>(); if (downloadAllIssuesAtOnce) { fetchTasks.add(CompletableFuture.runAsync(() -> issueSynchronizationService.fetchProjectIssues(binding, activeBranch, cancelMonitor), issueUpdaterExecutorService)); } else { fetchTasks.addAll(serverFileRelativePaths.stream() .map(serverFileRelativePath -> CompletableFuture.runAsync(() -> issueSynchronizationService .fetchFileIssues(binding, serverFileRelativePath, activeBranch, cancelMonitor), issueUpdaterExecutorService)) .toList()); } CompletableFuture.allOf(fetchTasks.toArray(new CompletableFuture[0])).join(); } private void refreshServerSecurityHotspots(SonarLintCancelMonitor cancelMonitor, Binding binding, String activeBranch, Set pathsInvolved, FilePathTranslation translation) { var serverFileRelativePaths = pathsInvolved.stream().map(translation::ideToServerPath).collect(Collectors.toSet()); var downloadAllSecurityHotspotsAtOnce = serverFileRelativePaths.size() > FETCH_ALL_ISSUES_THRESHOLD; var fetchTasks = new LinkedList>(); if (downloadAllSecurityHotspotsAtOnce) { fetchTasks.add(CompletableFuture.runAsync(() -> hotspotSynchronizationService.fetchProjectHotspots(binding, activeBranch, cancelMonitor), issueUpdaterExecutorService)); } else { fetchTasks.addAll(serverFileRelativePaths.stream() .map(serverFileRelativePath -> CompletableFuture .runAsync(() -> hotspotSynchronizationService.fetchFileHotspots(binding, activeBranch, serverFileRelativePath, cancelMonitor), issueUpdaterExecutorService)) .toList()); } CompletableFuture.allOf(fetchTasks.toArray(new CompletableFuture[0])).join(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/HotspotSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.nio.file.Path; import java.util.LinkedHashSet; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.HotspotDownloader; import org.sonarsource.sonarlint.core.serverconnection.ServerHotspotUpdater; import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer; import org.sonarsource.sonarlint.core.storage.StorageService; public class HotspotSynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StorageService storageService; private final LanguageSupportRepository languageSupportRepository; private final SonarQubeClientManager sonarQubeClientManager; public HotspotSynchronizationService(StorageService storageService, LanguageSupportRepository languageSupportRepository, SonarQubeClientManager sonarQubeClientManager) { this.storageService = storageService; this.languageSupportRepository = languageSupportRepository; this.sonarQubeClientManager = sonarQubeClientManager; } public void syncServerHotspotsForProject(ServerApi serverApi, String connectionId, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var serverVersion = getSonarServerVersion(serverApi, storage, cancelMonitor); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var hotspotsUpdater = new ServerHotspotUpdater(storage, new HotspotDownloader(enabledLanguagesToSync)); if (HotspotApi.supportHotspotsPull(serverApi.isSonarCloud(), serverVersion)) { LOG.info("[SYNC] Synchronizing hotspots for project '{}' on branch '{}'", projectKey, branchName); hotspotsUpdater.sync(serverApi.hotspot(), projectKey, branchName, enabledLanguagesToSync, cancelMonitor); } else { LOG.debug("Incremental hotspot sync is not supported. Skipping."); } } private static Version getSonarServerVersion(ServerApi serverApi, ConnectionStorage storage, SonarLintCancelMonitor cancelMonitor) { var serverInfoSynchronizer = new ServerInfoSynchronizer(storage); return serverInfoSynchronizer.readOrSynchronizeServerInfo(serverApi, cancelMonitor).version(); } public void fetchProjectHotspots(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(binding.connectionId(), serverApi -> downloadAllServerHotspots(binding.connectionId(), serverApi, binding.sonarProjectKey(), activeBranch, cancelMonitor)); } private void downloadAllServerHotspots(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var hotspotsUpdater = new ServerHotspotUpdater(storage, new HotspotDownloader(enabledLanguagesToSync)); hotspotsUpdater.updateAll(serverApi.hotspot(), projectKey, branchName, enabledLanguagesToSync, cancelMonitor); } public void fetchFileHotspots(Binding binding, String activeBranch, Path serverFilePath, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(binding.connectionId(), serverApi -> downloadAllServerHotspotsForFile(binding.connectionId(), serverApi, binding.sonarProjectKey(), serverFilePath, activeBranch, cancelMonitor)); } private void downloadAllServerHotspotsForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverRelativeFilePath, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var serverVersion = getSonarServerVersion(serverApi, storage, cancelMonitor); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var hotspotsUpdater = new ServerHotspotUpdater(storage, new HotspotDownloader(enabledLanguagesToSync)); hotspotsUpdater.updateForFile(serverApi.hotspot(), projectKey, serverRelativeFilePath, branchName, () -> serverVersion, cancelMonitor); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/IssueSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.nio.file.Path; import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverconnection.IssueDownloader; import org.sonarsource.sonarlint.core.serverconnection.ServerIssueUpdater; import org.sonarsource.sonarlint.core.serverconnection.TaintIssueDownloader; import org.sonarsource.sonarlint.core.storage.StorageService; public class IssueSynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StorageService storageService; private final LanguageSupportRepository languageSupportRepository; private final SonarQubeClientManager sonarQubeClientManager; public IssueSynchronizationService(StorageService storageService, LanguageSupportRepository languageSupportRepository, SonarQubeClientManager sonarQubeClientManager) { this.storageService = storageService; this.languageSupportRepository = languageSupportRepository; this.sonarQubeClientManager = sonarQubeClientManager; } public void syncServerIssuesForProject(ServerApi serverApi, String connectionId, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var issuesUpdater = new ServerIssueUpdater(storage, new IssueDownloader(enabledLanguagesToSync), new TaintIssueDownloader(enabledLanguagesToSync)); if (serverApi.isSonarCloud()) { LOG.debug("Incremental issue sync is not supported by SonarCloud. Skipping."); } else { LOG.info("[SYNC] Synchronizing issues for project '{}' on branch '{}'", projectKey, branchName); issuesUpdater.sync(serverApi, projectKey, branchName, enabledLanguagesToSync, cancelMonitor); } } public void fetchProjectIssues(Binding binding, String activeBranch, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(binding.connectionId(), serverApi -> downloadServerIssuesForProject(binding.connectionId(), serverApi, binding.sonarProjectKey(), activeBranch, cancelMonitor)); } private void downloadServerIssuesForProject(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var issuesUpdater = new ServerIssueUpdater(storage, new IssueDownloader(enabledLanguagesToSync()), new TaintIssueDownloader(enabledLanguagesToSync())); issuesUpdater.update(serverApi, projectKey, branchName, enabledLanguagesToSync(), cancelMonitor); } public void fetchFileIssues(Binding binding, Path serverFileRelativePath, String activeBranch, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(binding.connectionId(), serverApi -> downloadServerIssuesForFile(binding.connectionId(), serverApi, binding.sonarProjectKey(), serverFileRelativePath, activeBranch, cancelMonitor)); } public void downloadServerIssuesForFile(String connectionId, ServerApi serverApi, String projectKey, Path serverFileRelativePath, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var issuesUpdater = new ServerIssueUpdater(storage, new IssueDownloader(enabledLanguagesToSync), new TaintIssueDownloader(enabledLanguagesToSync)); issuesUpdater.updateFileIssuesIfNeeded(serverApi, projectKey, serverFileRelativePath, branchName, cancelMonitor); } private Set enabledLanguagesToSync() { return languageSupportRepository.getEnabledLanguagesInConnectedMode().stream() .filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/PluginsSynchronizedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import org.jetbrains.annotations.Nullable; public record PluginsSynchronizedEvent(@Nullable String connectionId) { } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/ScaSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.DependencyRisksSynchronizedEvent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.storage.UpdateSummary; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static java.util.stream.Collectors.toSet; public class ScaSynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StorageService storageService; private final ApplicationEventPublisher eventPublisher; private final boolean isScaSynchronizationEnabled; public ScaSynchronizationService(StorageService storageService, ApplicationEventPublisher eventPublisher, InitializeParams initializeParams) { this.storageService = storageService; this.eventPublisher = eventPublisher; this.isScaSynchronizationEnabled = initializeParams.getBackendCapabilities().contains(BackendCapability.SCA_SYNCHRONIZATION); } public void synchronize(ServerApi serverApi, String connectionId, String sonarProjectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { if (!isScaSynchronizationEnabled) { return; } if (!isScaSupported(connectionId)) { return; } LOG.info("[SYNC] Synchronizing dependency risks for project '{}' on branch '{}'", sonarProjectKey, branchName); var summary = updateServerDependencyRisksForProject(serverApi, connectionId, sonarProjectKey, branchName, cancelMonitor); if (summary.hasAnythingChanged()) { eventPublisher.publishEvent(new DependencyRisksSynchronizedEvent(connectionId, sonarProjectKey, branchName, summary)); } } private UpdateSummary updateServerDependencyRisksForProject(ServerApi serverApi, String connectionId, String sonarProjectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var issuesReleases = serverApi.sca().getIssuesReleases(sonarProjectKey, branchName, cancelMonitor); var findingsStore = storageService.connection(connectionId).project(sonarProjectKey).findings(); var previousDependencyRisks = findingsStore.loadDependencyRisks(branchName); var previousDependencyRiskKeys = previousDependencyRisks.stream().map(ServerDependencyRisk::key).collect(toSet()); var serverDependencyRisks = issuesReleases.issuesReleases().stream() .map(issueRelease -> new ServerDependencyRisk( issueRelease.key(), ServerDependencyRisk.Type.valueOf(issueRelease.type().name()), ServerDependencyRisk.Severity.valueOf(issueRelease.severity().name()), ServerDependencyRisk.SoftwareQuality.valueOf(issueRelease.quality().name()), ServerDependencyRisk.Status.valueOf(issueRelease.status().name()), issueRelease.release().packageName(), issueRelease.release().version(), issueRelease.vulnerabilityId(), issueRelease.cvssScore(), issueRelease.transitions().stream().map(Enum::name).map(ServerDependencyRisk.Transition::valueOf).toList())) .toList(); findingsStore.replaceAllDependencyRisksOfBranch(branchName, serverDependencyRisks); var newDependencyRiskKeys = serverDependencyRisks.stream().map(ServerDependencyRisk::key).collect(toSet()); var deletedDependencyRiskIds = previousDependencyRisks.stream() .map(ServerDependencyRisk::key) .filter(key -> !newDependencyRiskKeys.contains(key)) .collect(toSet()); var addedDependencyRisks = serverDependencyRisks.stream() .filter(issue -> !previousDependencyRiskKeys.contains(issue.key())) .toList(); var updatedDependencyRisks = serverDependencyRisks.stream() .filter(issue -> previousDependencyRiskKeys.contains(issue.key())) .toList(); return new UpdateSummary<>(deletedDependencyRiskIds, addedDependencyRisks, updatedDependencyRisks); } private boolean isScaSupported(String connectionId) { var serverInfo = storageService.connection(connectionId).serverInfo().read(); return serverInfo.map(info -> info.hasFeature(Feature.SCA)).orElse(false); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesChangedEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; public class SonarProjectBranchesChangedEvent { private final String connectionId; private final String sonarProjectKey; public SonarProjectBranchesChangedEvent(String connectionId, String sonarProjectKey) { this.connectionId = connectionId; this.sonarProjectKey = sonarProjectKey; } public String getConnectionId() { return connectionId; } public String getSonarProjectKey() { return sonarProjectKey; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SonarProjectBranchesSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.util.Optional; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.branches.ServerBranch; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranches; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static java.util.stream.Collectors.toSet; /** * This service manages the synchronization of the SonarProject branches from the Sonar server in the local storage. */ public class SonarProjectBranchesSynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final StorageService storageService; private final SonarQubeClientManager sonarQubeClientManager; private final ApplicationEventPublisher eventPublisher; public SonarProjectBranchesSynchronizationService(StorageService storageService, SonarQubeClientManager sonarQubeClientManager, ApplicationEventPublisher eventPublisher) { this.storageService = storageService; this.sonarQubeClientManager = sonarQubeClientManager; this.eventPublisher = eventPublisher; } public void sync(String connectionId, String sonarProjectKey, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(connectionId, serverApi -> { var branchesStorage = storageService.connection(connectionId).project(sonarProjectKey).branches(); Optional oldBranches = Optional.empty(); if (branchesStorage.exists()) { oldBranches = Optional.of(branchesStorage.read()); } var newBranches = getProjectBranches(serverApi, sonarProjectKey, cancelMonitor); branchesStorage.store(newBranches); if (oldBranches.isEmpty() || !oldBranches.get().equals(newBranches)) { LOG.debug("Project branches changed for project '{}'", sonarProjectKey); eventPublisher.publishEvent(new SonarProjectBranchesChangedEvent(connectionId, sonarProjectKey)); } }); } public ProjectBranches getProjectBranches(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { LOG.info("Synchronizing project branches for project '{}'", projectKey); var allBranches = serverApi.branches().getAllBranches(projectKey, cancelMonitor); var mainBranch = allBranches.stream().filter(ServerBranch::isMain).findFirst().map(ServerBranch::getName) .orElseThrow(() -> new IllegalStateException("No main branch for project '" + projectKey + "'")); return new ProjectBranches(allBranches.stream().map(ServerBranch::getName).collect(toSet()), mainBranch); } public String findMainBranch(String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { var branchesStorage = storageService.binding(new Binding(connectionId, projectKey)).branches(); if (branchesStorage.exists()) { var storedBranches = branchesStorage.read(); return storedBranches.getMainBranchName(); } else { return sonarQubeClientManager.withActiveClientAndReturn(connectionId, serverApi -> getProjectBranches(serverApi, projectKey, cancelMonitor)) .map(ProjectBranches::getMainBranchName).orElseThrow(); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.branch.MatchedSonarProjectBranchChangedEvent; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.ProgressIndicator; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.ConnectionCredentialsChangedEvent; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.DidSynchronizeConfigurationScopeParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; import org.sonarsource.sonarlint.core.serverconnection.LocalStorageSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.OrganizationSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent; import org.sonarsource.sonarlint.core.serverconnection.UserSynchronizer; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixSettingsSynchronizer; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.FULL_SYNCHRONIZATION; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.PROJECT_SYNCHRONIZATION; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SECURITY_HOTSPOTS; public class SynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final LanguageSupportRepository languageSupportRepository; private final SonarQubeClientManager sonarQubeClientManager; private final TaskManager taskManager; private final StorageService storageService; private final boolean branchSpecificSynchronizationEnabled; private final boolean fullSynchronizationEnabled; private final SynchronizationTimestampRepository scopeSynchronizationTimestampRepository = new SynchronizationTimestampRepository<>(); private final SynchronizationTimestampRepository bindingSynchronizationTimestampRepository = new SynchronizationTimestampRepository<>(); private final SynchronizationTimestampRepository branchSynchronizationTimestampRepository = new SynchronizationTimestampRepository<>(); private final TaintSynchronizationService taintSynchronizationService; private final ScaSynchronizationService scaSynchronizationService; private final IssueSynchronizationService issueSynchronizationService; private final HotspotSynchronizationService hotspotSynchronizationService; private final SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService; private final SonarProjectBranchTrackingService sonarProjectBranchTrackingService; private final ApplicationEventPublisher applicationEventPublisher; private final ExecutorServiceShutdownWatchable scheduledSynchronizer = new ExecutorServiceShutdownWatchable<>( FailSafeExecutors.newSingleThreadScheduledExecutor("SonarLint Local Storage Synchronizer")); private final Set ignoreBranchEventForScopes = ConcurrentHashMap.newKeySet(); private final boolean shouldSynchronizeHotspots; private final AiCodeFixRepository aiCodeFixRepository; private final PluginsService pluginsService; public SynchronizationService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, LanguageSupportRepository languageSupportRepository, SonarQubeClientManager sonarQubeClientManager, TaskManager taskManager, StorageService storageService, InitializeParams params, TaintSynchronizationService taintSynchronizationService, ScaSynchronizationService scaSynchronizationService, IssueSynchronizationService issueSynchronizationService, HotspotSynchronizationService hotspotSynchronizationService, SonarProjectBranchesSynchronizationService sonarProjectBranchesSynchronizationService, SonarProjectBranchTrackingService sonarProjectBranchTrackingService, ApplicationEventPublisher applicationEventPublisher, AiCodeFixRepository aiCodeFixRepository, PluginsService pluginsService) { this.client = client; this.configurationRepository = configurationRepository; this.languageSupportRepository = languageSupportRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.taskManager = taskManager; this.storageService = storageService; this.branchSpecificSynchronizationEnabled = params.getBackendCapabilities().contains(PROJECT_SYNCHRONIZATION); this.shouldSynchronizeHotspots = params.getBackendCapabilities().contains(SECURITY_HOTSPOTS); this.fullSynchronizationEnabled = params.getBackendCapabilities().contains(FULL_SYNCHRONIZATION); this.taintSynchronizationService = taintSynchronizationService; this.scaSynchronizationService = scaSynchronizationService; this.issueSynchronizationService = issueSynchronizationService; this.hotspotSynchronizationService = hotspotSynchronizationService; this.sonarProjectBranchesSynchronizationService = sonarProjectBranchesSynchronizationService; this.sonarProjectBranchTrackingService = sonarProjectBranchTrackingService; this.applicationEventPublisher = applicationEventPublisher; this.aiCodeFixRepository = aiCodeFixRepository; this.pluginsService = pluginsService; } @PostConstruct public void startScheduledSync() { if (!branchSpecificSynchronizationEnabled) { return; } var initialDelay = Long.parseLong(System.getProperty("sonarlint.internal.synchronization.initialDelay", "3600")); var syncPeriod = Long.parseLong(System.getProperty("sonarlint.internal.synchronization.period", "3600")); var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(scheduledSynchronizer); scheduledSynchronizer.getWrapped().scheduleAtFixedRate(() -> safeSyncAllConfigScopes(cancelMonitor), initialDelay, syncPeriod, TimeUnit.SECONDS); } // we must catch errors for the scheduling to not stop private void safeSyncAllConfigScopes(SonarLintCancelMonitor cancelMonitor) { try { synchronizeProjectsSync(configurationRepository.getBoundScopeByConnectionAndSonarProject(), cancelMonitor); } catch (Exception e) { LOG.error("Error during the auto-sync", e); } } private void synchronizeProjectsAsync(Map>> boundScopeByConnectionAndSonarProject) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(scheduledSynchronizer); scheduledSynchronizer.execute(() -> synchronizeProjectsSync(boundScopeByConnectionAndSonarProject, cancelMonitor)); } private void synchronizeProjectsSync(Map>> boundScopeByConnectionAndSonarProject, SonarLintCancelMonitor cancelMonitor) { if (boundScopeByConnectionAndSonarProject.isEmpty()) { return; } taskManager.createAndRunTask(null, UUID.randomUUID(), "Synchronizing projects...", null, false, false, progressIndicator -> { var connectionsCount = boundScopeByConnectionAndSonarProject.size(); var progressGap = 100f / connectionsCount; var progress = 0f; var synchronizedConfScopeIds = new HashSet(); for (var entry : boundScopeByConnectionAndSonarProject.entrySet()) { var connectionId = entry.getKey(); progressIndicator.notifyProgress("Synchronizing with '" + connectionId + "'...", Math.round(progress)); synchronizeProjectsOfTheSameConnection(connectionId, entry.getValue(), progressIndicator, synchronizedConfScopeIds, progress, progressGap, cancelMonitor); progress += progressGap; } if (!synchronizedConfScopeIds.isEmpty()) { applicationEventPublisher.publishEvent(new ConfigurationScopesSynchronizedEvent(synchronizedConfScopeIds)); client.didSynchronizeConfigurationScopes(new DidSynchronizeConfigurationScopeParams(synchronizedConfScopeIds)); } }, cancelMonitor); } private void synchronizeProjectsOfTheSameConnection(String connectionId, Map> boundScopeBySonarProject, ProgressIndicator progressIndicator, Set synchronizedConfScopeIds, float progress, float progressGap, SonarLintCancelMonitor cancelMonitor) { if (boundScopeBySonarProject.isEmpty()) { return; } sonarQubeClientManager.withActiveClient(connectionId, serverApi -> { var subProgressGap = progressGap / boundScopeBySonarProject.size(); var subProgress = progress; for (var entry : boundScopeBySonarProject.entrySet()) { synchronizeProjectWithProgress(serverApi, connectionId, entry.getKey(), entry.getValue(), progressIndicator, cancelMonitor, synchronizedConfScopeIds, subProgress); subProgress += subProgressGap; } }); } private void synchronizeProjectWithProgress(ServerApi serverApi, String connectionId, String sonarProjectKey, Collection boundScopes, ProgressIndicator progressIndicator, SonarLintCancelMonitor cancelMonitor, Set synchronizedConfigScopeIds, float subProgress) { var allScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, sonarProjectKey); var allScopesByOptBranch = allScopes.stream() .collect(groupingBy(b -> sonarProjectBranchTrackingService.awaitEffectiveSonarProjectBranch(b.getConfigScopeId()))); allScopesByOptBranch .forEach((branchNameOpt, scopes) -> branchNameOpt.ifPresent(branchName -> { var branchBinding = new BranchBinding(new Binding(connectionId, sonarProjectKey), branchName); if (shouldSynchronizeBranch(branchBinding)) { branchSynchronizationTimestampRepository.setLastSynchronizationTimestampToNow(branchBinding); progressIndicator.notifyProgress("Synchronizing project '" + sonarProjectKey + "'...", (int) subProgress); issueSynchronizationService.syncServerIssuesForProject(serverApi, connectionId, sonarProjectKey, branchName, cancelMonitor); taintSynchronizationService.synchronizeTaintVulnerabilities(serverApi, connectionId, sonarProjectKey, branchName, cancelMonitor); scaSynchronizationService.synchronize(serverApi, connectionId, sonarProjectKey, branchName, cancelMonitor); if (shouldSynchronizeHotspots) { hotspotSynchronizationService.syncServerHotspotsForProject(serverApi, connectionId, sonarProjectKey, branchName, cancelMonitor); } synchronizedConfigScopeIds.addAll(boundScopes.stream().map(BoundScope::getConfigScopeId).collect(toSet())); } })); } public Version readOrSynchronizeServerVersion(String connectionId, ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { var serverInfoSynchronizer = new ServerInfoSynchronizer(storageService.connection(connectionId)); return serverInfoSynchronizer.readOrSynchronizeServerInfo(serverApi, cancelMonitor).version(); } @EventListener public void onConfigurationsScopeAdded(ConfigurationScopesAddedWithBindingEvent event) { if (!fullSynchronizationEnabled) { return; } LOG.debug("Synchronizing new configuration scopes: {}", event.getConfigScopeIds()); var scopesToSynchronize = event.getConfigScopeIds() .stream().map(configurationRepository::getBoundScope) .filter(Objects::nonNull) .collect(groupingBy(BoundScope::getConnectionId)); scopesToSynchronize.forEach(this::synchronizeConnectionAndProjectsIfNeededAsync); } @EventListener public void onConfigurationScopeRemoved(ConfigurationScopeRemovedEvent event) { var scopeId = event.getRemovedConfigurationScopeId(); LOG.debug("Config scope {} removed, managing caches", scopeId); scopeSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(scopeId); var previousBinding = event.removedBindingConfiguration(); if (previousBinding.isBound()) { var connectionId = requireNonNull(previousBinding.connectionId()); var projectKey = requireNonNull(previousBinding.sonarProjectKey()); var scopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); if (scopes.isEmpty()) { // no remaining scope bound to this connection and project, clear the cache LOG.debug("Clearing the synchronization cache for {}, binding={}", scopeId, previousBinding); var binding = new Binding(connectionId, projectKey); bindingSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(binding); branchSynchronizationTimestampRepository.clearLastSynchronizationTimestampIf(branchBinding -> branchBinding.getBinding().equals(binding)); } else { LOG.debug("Other config scopes are still bound to {}, see {}, keeping the cache", previousBinding, scopes); } } else { LOG.debug("Removed config scope was not bound, {}, keeping the cache", previousBinding); } } @EventListener public void onBindingChanged(BindingConfigChangedEvent event) { if (!fullSynchronizationEnabled) { return; } var configScopeId = event.configScopeId(); scopeSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(configScopeId); if (event.previousConfig().isBound()) { // when unbinding, we want to let future rebinds trigger a sync var previousBinding = new Binding(requireNonNull(event.previousConfig().connectionId()), requireNonNull(event.previousConfig().sonarProjectKey())); bindingSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(previousBinding); branchSynchronizationTimestampRepository.clearLastSynchronizationTimestampIf(branchBinding -> branchBinding.getBinding().equals(previousBinding)); } var newConnectionId = event.newConfig().connectionId(); if (newConnectionId != null) { synchronizeConnectionAndProjectsIfNeededAsync( newConnectionId, List.of(new BoundScope(configScopeId, newConnectionId, requireNonNull(event.newConfig().sonarProjectKey())))); } } @EventListener public void onConnectionCredentialsChanged(ConnectionCredentialsChangedEvent event) { if (!fullSynchronizationEnabled) { return; } var connectionId = event.getConnectionId(); LOG.debug("Synchronizing connection '{}' after credentials changed", connectionId); var bindingsForUpdatedConnection = configurationRepository.getBoundScopesToConnection(connectionId); // Clear the synchronization timestamp for all the scopes so that sync is not skipped bindingsForUpdatedConnection.forEach(boundScope -> { scopeSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(boundScope.getConfigScopeId()); var binding = new Binding(connectionId, boundScope.getSonarProjectKey()); bindingSynchronizationTimestampRepository.clearLastSynchronizationTimestamp(binding); branchSynchronizationTimestampRepository.clearLastSynchronizationTimestampIf(branchBinding -> branchBinding.getBinding().equals(binding)); }); synchronizeConnectionAndProjectsIfNeededAsync(connectionId, bindingsForUpdatedConnection); } private void synchronizeConnectionAndProjectsIfNeededAsync(String connectionId, Collection boundScopes) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(scheduledSynchronizer); scheduledSynchronizer.execute( () -> sonarQubeClientManager.withActiveClient(connectionId, serverApi -> synchronizeConnectionAndProjectsIfNeededSync(connectionId, serverApi, boundScopes, cancelMonitor))); } private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, ServerApi serverApi, Collection boundScopes, SonarLintCancelMonitor cancelMonitor) { var scopesToSync = boundScopes.stream().filter(this::shouldSynchronizeScope).toList(); if (scopesToSync.isEmpty()) { return; } scopesToSync.forEach(scope -> scopeSynchronizationTimestampRepository.setLastSynchronizationTimestampToNow(scope.getConfigScopeId())); // We will already trigger a sync of the project storage so we can temporarily ignore branch changed event for these config scopes ignoreBranchEventForScopes.addAll(scopesToSync.stream().map(BoundScope::getConfigScopeId).collect(toSet())); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream() .filter(SonarLanguage::shouldSyncInConnectedMode).collect(Collectors.toCollection(LinkedHashSet::new)); var storage = storageService.connection(connectionId); var serverInfoSynchronizer = new ServerInfoSynchronizer(storage); var storageSynchronizer = new LocalStorageSynchronizer(enabledLanguagesToSync, serverInfoSynchronizer, storage); synchronizePlugins(connectionId); var aiCodeFixSynchronizer = new AiCodeFixSettingsSynchronizer(storage, new OrganizationSynchronizer(storage), aiCodeFixRepository); var userSynchronizer = new UserSynchronizer(storage); try { LOG.debug("Synchronizing storage of connection '{}'", connectionId); userSynchronizer.synchronize(serverApi, cancelMonitor); var summary = storageSynchronizer.synchronizeServerInfosAndPlugins(serverApi, cancelMonitor); scopesToSync = scopesToSync.stream() .filter(boundScope -> shouldSynchronizeBinding(new Binding(connectionId, boundScope.getSonarProjectKey()))).toList(); var scopesPerProjectKey = scopesToSync.stream() .collect(groupingBy(BoundScope::getSonarProjectKey, mapping(BoundScope::getConfigScopeId, toSet()))); aiCodeFixSynchronizer.synchronize(serverApi, summary.version(), scopesPerProjectKey.keySet(), cancelMonitor); scopesPerProjectKey.forEach((projectKey, configScopeIds) -> { var binding = new Binding(connectionId, projectKey); bindingSynchronizationTimestampRepository.setLastSynchronizationTimestampToNow(binding); LOG.debug("Synchronizing storage of Sonar project '{}' for connection '{}'", projectKey, connectionId); var analyzerConfigUpdateSummary = storageSynchronizer.synchronizeAnalyzerConfig(serverApi, projectKey, cancelMonitor); // XXX we might want to group those 2 events under one if (!analyzerConfigUpdateSummary.getUpdatedSettingsValueByKey().isEmpty()) { applicationEventPublisher.publishEvent( new SonarServerSettingsChangedEvent(connectionId, configScopeIds, analyzerConfigUpdateSummary.getUpdatedSettingsValueByKey())); } applicationEventPublisher.publishEvent(new AnalyzerConfigurationSynchronized(binding, configScopeIds)); sonarProjectBranchesSynchronizationService.sync(connectionId, projectKey, cancelMonitor); }); synchronizeProjectsSync( Map.of(connectionId, scopesToSync.stream().map(scope -> new BoundScope(scope.getConfigScopeId(), connectionId, scope.getSonarProjectKey())) .collect(groupingBy(BoundScope::getSonarProjectKey, toCollection(ArrayList::new)))), cancelMonitor); } catch (Exception e) { LOG.error("Error during synchronization", e); if (e instanceof UnauthorizedException || e instanceof ForbiddenException) { throw e; } } finally { ignoreBranchEventForScopes.removeAll(scopesToSync.stream().map(BoundScope::getConfigScopeId).collect(toSet())); } } private void synchronizePlugins(String connectionId) { var plugins = pluginsService.getPlugins(connectionId); // synchronization is synchronous, wait for downloads to happen plugins.artifactsResult().getAllDownloadsFuture() .ifPresent(future -> { try { future.get(5, TimeUnit.MINUTES); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } catch (ExecutionException | TimeoutException e) { throw new RuntimeException(e); } }); } private boolean shouldSynchronizeBinding(Binding binding) { boolean result = bindingSynchronizationTimestampRepository.getLastSynchronizationDate(binding) .map(lastSync -> lastSync.isBefore(Instant.now().minus(getSyncPeriod(), ChronoUnit.SECONDS))) .orElse(true); if (!result) { LOG.debug("Skipping synchronization of binding '{}' because it was synchronized recently", binding); } return result; } private boolean shouldSynchronizeScope(BoundScope configScope) { boolean result = scopeSynchronizationTimestampRepository.getLastSynchronizationDate(configScope.getConfigScopeId()) .map(lastSync -> lastSync.isBefore(Instant.now().minus(getSyncPeriod(), ChronoUnit.SECONDS))) .orElse(true); if (!result) { LOG.debug("Skipping synchronization of configuration scope '{}' because it was synchronized recently", configScope.getConfigScopeId()); } return result; } private boolean shouldSynchronizeBranch(BranchBinding branchBinding) { boolean result = branchSynchronizationTimestampRepository.getLastSynchronizationDate(branchBinding) .map(lastSync -> lastSync.isBefore(Instant.now().minus(getSyncPeriod(), ChronoUnit.SECONDS))) .orElse(true); if (!result) { LOG.debug("Skipping synchronization of branch '{}' because it was synchronized recently", branchBinding.getBranchName()); } return result; } private static long getSyncPeriod() { return Long.parseLong(System.getProperty("sonarlint.internal.synchronization.scope.period", "300")); } @EventListener public void onSonarProjectBranchChanged(MatchedSonarProjectBranchChangedEvent changedEvent) { if (!branchSpecificSynchronizationEnabled) { return; } var configurationScopeId = changedEvent.getConfigurationScopeId(); if (ignoreBranchEventForScopes.contains(configurationScopeId)) { return; } configurationRepository.getEffectiveBinding(configurationScopeId).ifPresent(binding -> synchronizeProjectsAsync(Map.of(requireNonNull(binding.connectionId()), Map.of(binding.sonarProjectKey(), List.of(new BoundScope(configurationScopeId, binding)))))); } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(scheduledSynchronizer, 5, TimeUnit.SECONDS)) { LOG.warn("Unable to stop synchronizer executor service in a timely manner"); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/SynchronizationTimestampRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; public class SynchronizationTimestampRepository { private final Map lastSynchronizationTimestampPerSource = new ConcurrentHashMap<>(); public Optional getLastSynchronizationDate(T source) { return Optional.ofNullable(lastSynchronizationTimestampPerSource.get(source)); } public void setLastSynchronizationTimestampToNow(T source) { lastSynchronizationTimestampPerSource.put(source, Instant.now()); } public void clearLastSynchronizationTimestamp(T source) { lastSynchronizationTimestampPerSource.remove(source); } public void clearLastSynchronizationTimestampIf(Predicate predicate) { lastSynchronizationTimestampPerSource.keySet().removeIf(predicate); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/TaintSynchronizationService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import java.util.LinkedHashSet; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.TaintVulnerabilitiesSynchronizedEvent; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverconnection.IssueDownloader; import org.sonarsource.sonarlint.core.serverconnection.ServerIssueUpdater; import org.sonarsource.sonarlint.core.serverconnection.TaintIssueDownloader; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.serverconnection.storage.UpdateSummary; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static java.util.stream.Collectors.groupingBy; public class TaintSynchronizationService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConfigurationRepository configurationRepository; private final SonarProjectBranchTrackingService branchTrackingService; private final StorageService storageService; private final LanguageSupportRepository languageSupportRepository; private final SonarQubeClientManager sonarQubeClientManager; private final ApplicationEventPublisher eventPublisher; public TaintSynchronizationService(ConfigurationRepository configurationRepository, SonarProjectBranchTrackingService branchTrackingService, StorageService storageService, LanguageSupportRepository languageSupportRepository, SonarQubeClientManager sonarQubeClientManager, ApplicationEventPublisher eventPublisher) { this.configurationRepository = configurationRepository; this.branchTrackingService = branchTrackingService; this.storageService = storageService; this.languageSupportRepository = languageSupportRepository; this.sonarQubeClientManager = sonarQubeClientManager; this.eventPublisher = eventPublisher; } public void synchronizeTaintVulnerabilities(String connectionId, String projectKey, SonarLintCancelMonitor cancelMonitor) { sonarQubeClientManager.withActiveClient(connectionId, serverApi -> { var allScopes = configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey); var allScopesByOptBranch = allScopes.stream() .collect(groupingBy(b -> branchTrackingService.awaitEffectiveSonarProjectBranch(b.getConfigScopeId()))); allScopesByOptBranch .forEach((branchNameOpt, scopes) -> branchNameOpt.ifPresent(branchName -> synchronizeTaintVulnerabilities(serverApi, connectionId, projectKey, branchName, cancelMonitor))); }); } public void synchronizeTaintVulnerabilities(ServerApi serverApi, String connectionId, String projectKey, String branch, SonarLintCancelMonitor cancelMonitor) { if (languageSupportRepository.areTaintVulnerabilitiesSupported()) { var summary = updateServerTaintIssuesForProject(connectionId, serverApi, projectKey, branch, cancelMonitor); if (summary.hasAnythingChanged()) { eventPublisher.publishEvent(new TaintVulnerabilitiesSynchronizedEvent(connectionId, projectKey, branch, summary)); } } } private UpdateSummary updateServerTaintIssuesForProject(String connectionId, ServerApi serverApi, String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { var storage = storageService.connection(connectionId); var enabledLanguagesToSync = languageSupportRepository.getEnabledLanguagesInConnectedMode().stream().filter(SonarLanguage::shouldSyncInConnectedMode) .collect(Collectors.toCollection(LinkedHashSet::new)); var issuesUpdater = new ServerIssueUpdater(storage, new IssueDownloader(enabledLanguagesToSync), new TaintIssueDownloader(enabledLanguagesToSync)); if (serverApi.isSonarCloud()) { return issuesUpdater.downloadProjectTaints(serverApi, projectKey, branchName, enabledLanguagesToSync, cancelMonitor); } else { LOG.info("[SYNC] Synchronizing taint issues for project '{}' on branch '{}'", projectKey, branchName); return issuesUpdater.syncTaints(serverApi, projectKey, branchName, enabledLanguagesToSync, cancelMonitor); } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/sync/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.sync; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryServerAttributesProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.util.ArrayList; import java.util.Collection; import java.util.Objects; import java.util.function.Predicate; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.serverconnection.Organization; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.storage.StorageService; public class TelemetryServerAttributesProvider { private final ConfigurationRepository configurationRepository; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final ActiveRulesService activeRulesService; private final RulesRepository rulesRepository; private final NodeJsService nodeJsService; private final StorageService storageService; public TelemetryServerAttributesProvider(ConfigurationRepository configurationRepository, ConnectionConfigurationRepository connectionConfigurationRepository, ActiveRulesService activeRulesService, RulesRepository rulesRepository, NodeJsService nodeJsService, StorageService storageService) { this.configurationRepository = configurationRepository; this.connectionConfigurationRepository = connectionConfigurationRepository; this.activeRulesService = activeRulesService; this.rulesRepository = rulesRepository; this.nodeJsService = nodeJsService; this.storageService = storageService; } public TelemetryServerAttributes getTelemetryServerLiveAttributes() { var allBindings = configurationRepository.getAllBoundScopes(); var usesConnectedMode = !allBindings.isEmpty(); var usesSonarCloud = allBindings.stream().anyMatch(isSonarCloudConnectionConfiguration()); var childBindingCount = countChildBindings(); var sonarQubeServerBindingCount = countSonarQubeServerBindings(allBindings); var sonarQubeCloudEUBindingCount = countSonarQubeCloudBindings(allBindings, SonarCloudRegion.EU); var sonarQubeCloudUSBindingCount = countSonarQubeCloudBindings(allBindings, SonarCloudRegion.US); var devNotificationsDisabled = allBindings.stream().anyMatch(this::hasDisableNotifications); var nonDefaultEnabledRules = new ArrayList(); var defaultDisabledRules = new ArrayList(); activeRulesService.getStandaloneRuleConfig().forEach((ruleKey, standaloneRuleConfigDto) -> { var optionalEmbeddedRule = rulesRepository.getEmbeddedRule(ruleKey); if (optionalEmbeddedRule.isEmpty()) { return; } var activeByDefault = optionalEmbeddedRule.get().isActiveByDefault(); var isActive = standaloneRuleConfigDto.isActive(); if (activeByDefault && !isActive) { defaultDisabledRules.add(ruleKey); } else if (!activeByDefault && isActive) { nonDefaultEnabledRules.add(ruleKey); } }); var nodeJsVersion = getNodeJsVersion(); var connectionsAttributes = connectionConfigurationRepository.getConnectionsById().keySet().stream() .map(storageService::connection) .map(c -> { var userId = c.user().read().orElse(null); var serverId = c.serverInfo().read().map(StoredServerInfo::serverId).orElse(null); var orgId = c.organization().read().map(Organization::id).orElse(null); if (userId == null && serverId == null && orgId == null) { return null; } return new TelemetryConnectionAttributes(userId, serverId, orgId); }) .filter(Objects::nonNull) .toList(); return new TelemetryServerAttributes(usesConnectedMode, usesSonarCloud, childBindingCount, sonarQubeServerBindingCount, sonarQubeCloudEUBindingCount, sonarQubeCloudUSBindingCount, devNotificationsDisabled, nonDefaultEnabledRules, defaultDisabledRules, nodeJsVersion, connectionsAttributes); } private int countSonarQubeCloudBindings(Collection allBindings, SonarCloudRegion region) { return (int) allBindings.stream() .filter(binding -> { if (connectionConfigurationRepository.getConnectionById(binding.getConnectionId()) instanceof SonarCloudConnectionConfiguration scBinding) { return region.equals(scBinding.getRegion()); } return false; }).count(); } private int countSonarQubeServerBindings(Collection allBindings) { return (int) allBindings.stream() .filter(binding -> connectionConfigurationRepository.getConnectionById(binding.getConnectionId()) instanceof SonarQubeConnectionConfiguration) .count(); } // We are looking for leaf config scope IDs that are bound to a different project key than their parents private int countChildBindings() { return (int) configurationRepository.getLeafConfigScopeIds().stream() .filter(scopeId -> { var configScope = configurationRepository.getConfigurationScope(scopeId); if (configScope != null && configScope.parentId() != null) { var parentBindingConfig = configurationRepository.getBindingConfiguration(configScope.parentId()); var leafBindingConfig = configurationRepository.getBindingConfiguration(scopeId); if (parentBindingConfig != null && leafBindingConfig != null) { var parentProjectKey = parentBindingConfig.sonarProjectKey(); var leafProjectKey = leafBindingConfig.sonarProjectKey(); return parentProjectKey != null && leafProjectKey != null && !parentProjectKey.equals(leafProjectKey); } } return false; }) .count(); } @CheckForNull private String getNodeJsVersion() { return nodeJsService.getActiveNodeJsVersion().map(Objects::toString).orElse(null); } private boolean hasDisableNotifications(BoundScope binding) { return Objects.requireNonNull(connectionConfigurationRepository.getConnectionById(binding.getConnectionId())).isDisableNotifications(); } private Predicate isSonarCloudConnectionConfiguration() { return binding -> connectionConfigurationRepository.getConnectionById(binding.getConnectionId()) instanceof SonarCloudConnectionConfiguration; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.time.OffsetDateTime; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.AnalysisFinishedEvent; import org.sonarsource.sonarlint.core.analysis.AutomaticAnalysisSettingChangedEvent; import org.sonarsource.sonarlint.core.analysis.IssuesRaisedEvent; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.FixSuggestionReceivedEvent; import org.sonarsource.sonarlint.core.event.LocalOnlyIssueStatusChangedEvent; import org.sonarsource.sonarlint.core.event.MatchingSessionEndedEvent; import org.sonarsource.sonarlint.core.event.ServerIssueStatusChangedEvent; import org.sonarsource.sonarlint.core.event.TelemetryUpdatedEvent; import org.sonarsource.sonarlint.core.promotion.campaign.CampaignResolvedEvent; import org.sonarsource.sonarlint.core.promotion.campaign.CampaignShownEvent; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.GetStatusResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingTriggeredParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionResolvedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.HelpAndFeedbackClickedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.ToolCalledParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Optional.ofNullable; import static java.util.concurrent.TimeUnit.MINUTES; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.TELEMETRY; public class TelemetryService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final long TELEMETRY_UPLOAD_DELAY = TimeUnit.HOURS.toMinutes(TelemetryManager.MIN_HOURS_BETWEEN_UPLOAD + 1L); private final ScheduledExecutorService scheduledExecutor; private final TelemetryManager telemetryManager; private final TelemetryServerAttributesProvider telemetryServerAttributesProvider; private final SonarLintRpcClient client; private final boolean isTelemetryFeatureEnabled; private final ApplicationEventPublisher applicationEventPublisher; public TelemetryService(InitializeParams initializeParams, SonarLintRpcClient sonarlintClient, TelemetryServerAttributesProvider telemetryServerAttributesProvider, TelemetryManager telemetryManager, ApplicationEventPublisher applicationEventPublisher) { this.isTelemetryFeatureEnabled = initializeParams.getBackendCapabilities().contains(TELEMETRY); this.client = sonarlintClient; this.telemetryServerAttributesProvider = telemetryServerAttributesProvider; this.telemetryManager = telemetryManager; this.applicationEventPublisher = applicationEventPublisher; this.scheduledExecutor = FailSafeExecutors.newSingleThreadScheduledExecutor("SonarLint Telemetry"); initTelemetryAndScheduleUpload(initializeParams); } private void initTelemetryAndScheduleUpload(InitializeParams initializeParams) { if (!isTelemetryFeatureEnabled) { LOG.info("Telemetry disabled on server startup"); return; } updateTelemetry(localStorage -> { localStorage.setInitialNewCodeFocus(initializeParams.isFocusOnNewCode()); localStorage.setInitialAutomaticAnalysisEnablement(initializeParams.isAutomaticAnalysisEnabled()); }); var initialDelay = Integer.parseInt(System.getProperty("sonarlint.internal.telemetry.initialDelay", "1")); scheduledExecutor.scheduleWithFixedDelay(this::upload, initialDelay, TELEMETRY_UPLOAD_DELAY, MINUTES); } private void upload() { var telemetryLiveAttributes = getTelemetryLiveAttributes(); if (Objects.nonNull(telemetryLiveAttributes)) { telemetryManager.uploadAndClearTelemetry(telemetryLiveAttributes); } } public GetStatusResponse getStatus() { return new GetStatusResponse(isEnabled()); } public void enableTelemetry() { if (!isTelemetryFeatureEnabled) { LOG.warn("Telemetry was disabled on server startup. Ignoring client request."); return; } var telemetryLiveAttributes = getTelemetryLiveAttributes(); if (Objects.nonNull(telemetryLiveAttributes)) { telemetryManager.enable(telemetryLiveAttributes); applicationEventPublisher.publishEvent(new TelemetryUpdatedEvent(true)); } } public void disableTelemetry() { var telemetryLiveAttributes = getTelemetryLiveAttributes(); if (Objects.nonNull(telemetryLiveAttributes)) { telemetryManager.disable(telemetryLiveAttributes); applicationEventPublisher.publishEvent(new TelemetryUpdatedEvent(false)); } } @Nullable private TelemetryLiveAttributes getTelemetryLiveAttributes() { try { var serverLiveAttributes = telemetryServerAttributesProvider.getTelemetryServerLiveAttributes(); var clientLiveAttributes = client.getTelemetryLiveAttributes().get(10, TimeUnit.SECONDS); return new TelemetryLiveAttributes(serverLiveAttributes, clientLiveAttributes); } catch (InterruptedException e) { Thread.currentThread().interrupt(); if (InternalDebug.isEnabled()) { LOG.error("Failed to fetch telemetry payload", e); } } catch (Exception e) { if (InternalDebug.isEnabled()) { LOG.error("Failed to fetch telemetry payload", e); } } return null; } public boolean isEnabled() { return isTelemetryFeatureEnabled && telemetryManager.isTelemetryEnabledByUser(); } public OffsetDateTime installTime() { return telemetryManager.installTime(); } private void updateTelemetry(Consumer updater) { if (isEnabled()) { telemetryManager.updateTelemetry(updater); } } public void hotspotOpenedInBrowser() { updateTelemetry(TelemetryLocalStorage::incrementOpenHotspotInBrowserCount); } public void showHotspotRequestReceived() { updateTelemetry(TelemetryLocalStorage::incrementShowHotspotRequestCount); } public void showIssueRequestReceived() { updateTelemetry(TelemetryLocalStorage::incrementShowIssueRequestCount); } public void taintVulnerabilitiesInvestigatedLocally() { updateTelemetry(TelemetryLocalStorage::incrementTaintVulnerabilitiesInvestigatedLocallyCount); } public void taintVulnerabilitiesInvestigatedRemotely() { updateTelemetry(TelemetryLocalStorage::incrementTaintVulnerabilitiesInvestigatedRemotelyCount); } public void helpAndFeedbackLinkClicked(HelpAndFeedbackClickedParams params) { updateTelemetry(localStorage -> localStorage.helpAndFeedbackLinkClicked(params.getItemId())); } public void analysisReportingTriggered(AnalysisReportingTriggeredParams params) { updateTelemetry(localStorage -> localStorage.analysisReportingTriggered(params.getAnalysisType())); } public void fixSuggestionResolved(FixSuggestionResolvedParams params) { updateTelemetry(localStorage -> localStorage.fixSuggestionResolved(params.getSuggestionId(), params.getStatus(), params.getSnippetIndex())); } public void smartNotificationsReceived(String eventType) { updateTelemetry(localStorage -> localStorage.incrementDevNotificationsCount(eventType)); } public void analysisDoneOnSingleLanguage(@Nullable Language language, int analysisTimeMs) { updateTelemetry(localStorage -> { var languageName = ofNullable(language) .map(Enum::name) .map(SonarLanguage::valueOf) .map(SonarLanguage::getSonarLanguageKey) .orElse("others"); localStorage.setUsedAnalysis(languageName, analysisTimeMs); }); } public void analysisDoneOnMultipleFiles() { updateTelemetry(TelemetryLocalStorage::setUsedAnalysis); } public void smartNotificationsClicked(String eventType) { updateTelemetry(localStorage -> localStorage.incrementDevNotificationsClicked(eventType)); } public void addQuickFixAppliedForRule(String ruleKey) { updateTelemetry(localStorage -> localStorage.addQuickFixAppliedForRule(ruleKey)); } public void addReportedRules(Set ruleKeys) { updateTelemetry(s -> s.addReportedRules(ruleKeys)); } public void hotspotStatusChanged() { updateTelemetry(TelemetryLocalStorage::incrementHotspotStatusChangedCount); } public void newCodeFocusChanged() { updateTelemetry(TelemetryLocalStorage::incrementNewCodeFocusChange); } private void issueStatusChanged(String ruleKey) { updateTelemetry(telemetryLocalStorage -> telemetryLocalStorage.addIssueStatusChanged(ruleKey)); } public void addedManualBindings() { updateTelemetry(TelemetryLocalStorage::incrementManualAddedBindingsCount); } public void addedImportedBindings() { updateTelemetry(TelemetryLocalStorage::incrementImportedAddedBindingsCount); } public void addedAutomaticBindings() { updateTelemetry(TelemetryLocalStorage::incrementAutoAddedBindingsCount); } public void acceptedBindingSuggestion(BindingSuggestionOrigin bindingSuggestionOrigin) { if (bindingSuggestionOrigin.equals(BindingSuggestionOrigin.REMOTE_URL)) { updateTelemetry(TelemetryLocalStorage::incrementNewBindingsRemoteUrlCount); } if (bindingSuggestionOrigin.equals(BindingSuggestionOrigin.PROJECT_NAME)) { updateTelemetry(TelemetryLocalStorage::incrementNewBindingsProjectNameCount); } if (bindingSuggestionOrigin.equals(BindingSuggestionOrigin.SHARED_CONFIGURATION)) { updateTelemetry(TelemetryLocalStorage::incrementNewBindingsSharedConfigurationCount); } if (bindingSuggestionOrigin.equals(BindingSuggestionOrigin.PROPERTIES_FILE)) { updateTelemetry(TelemetryLocalStorage::incrementNewBindingsPropertiesFileCount); } } public void exportedConnectedMode() { updateTelemetry(TelemetryLocalStorage::incrementExportedConnectedModeCount); } public void suggestedRemoteBinding() { updateTelemetry(TelemetryLocalStorage::incrementSuggestedRemoteBindingsCount); } public void mcpIntegrationEnabled() { updateTelemetry(storage -> storage.setMcpIntegrationEnabled(true)); } public void mcpTransportModeUsed(McpTransportMode transportMode) { updateTelemetry(storage -> storage.setMcpTransportModeUsed(transportMode)); } public void toolCalled(ToolCalledParams params) { updateTelemetry(storage -> storage.incrementToolCalledCount(params.getToolName(), params.isSucceeded())); } public void taintInvestigatedLocally() { updateTelemetry(TelemetryLocalStorage::incrementTaintInvestigatedLocallyCount); } public void taintInvestigatedRemotely() { updateTelemetry(TelemetryLocalStorage::incrementTaintInvestigatedRemotelyCount); } public void hotspotInvestigatedLocally() { updateTelemetry(TelemetryLocalStorage::incrementHotspotInvestigatedLocallyCount); } public void hotspotInvestigatedRemotely() { updateTelemetry(TelemetryLocalStorage::incrementHotspotInvestigatedRemotelyCount); } public void issueInvestigatedLocally() { updateTelemetry(TelemetryLocalStorage::incrementIssueInvestigatedLocallyCount); } public void dependencyRiskInvestigatedRemotely() { updateTelemetry(TelemetryLocalStorage::incrementDependencyRiskInvestigatedRemotelyCount); } public void dependencyRiskInvestigatedLocally() { updateTelemetry(TelemetryLocalStorage::incrementDependencyRiskInvestigatedLocallyCount); } public void findingsFiltered(String filterType) { updateTelemetry(localStorage -> localStorage.findingsFiltered(filterType)); } public void automaticAnalysisSettingToggled() { updateTelemetry(TelemetryLocalStorage::incrementAutomaticAnalysisToggledCount); } public void mcpServerConfigurationRequested() { updateTelemetry(TelemetryLocalStorage::incrementMcpServerConfigurationRequestedCount); } public void mcpRuleFileRequested() { updateTelemetry(TelemetryLocalStorage::incrementMcpRuleFileRequestedCount); } public void ideLabsLinkClicked(String linkId) { updateTelemetry(storage -> storage.ideLabsLinkClicked(linkId)); } public void ideLabsFeedbackLinkClicked(String featureId) { updateTelemetry(storage -> storage.ideLabsFeedbackLinkClicked(featureId)); } public void aiHookInstalled(AiAgent aiAgent) { updateTelemetry(storage -> storage.aiHookInstalled(aiAgent)); } @EventListener public void onMatchingSessionEnded(MatchingSessionEndedEvent event) { updateTelemetry(telemetryLocalStorage -> { telemetryLocalStorage.addNewlyFoundIssues(event.newIssuesFound()); telemetryLocalStorage.addFixedIssues(event.issuesFixed()); }); } @EventListener public void onAutomaticAnalysisSettingChanged(AutomaticAnalysisSettingChangedEvent event) { automaticAnalysisSettingToggled(); } @EventListener public void onServerIssueStatusChanged(ServerIssueStatusChangedEvent event) { issueStatusChanged(event.getFinding().getRuleKey()); } @EventListener public void onLocalOnlyIssueStatusChanged(LocalOnlyIssueStatusChangedEvent event) { issueStatusChanged(event.getIssue().getRuleKey()); } @EventListener public void onAnalysisFinished(AnalysisFinishedEvent event) { var languagePerFile = event.getLanguagePerFile(); if (languagePerFile.size() == 1 && event.succeededForAllFiles()) { var fileLanguage = languagePerFile.entrySet().iterator().next().getValue(); analysisDoneOnSingleLanguage(fileLanguage == null ? null : Language.valueOf(fileLanguage.name()), (int) event.getAnalysisDuration().toMillis()); } else { analysisDoneOnMultipleFiles(); } addReportedRules(event.getReportedRuleKeys()); } @EventListener public void onFixSuggestionReceived(FixSuggestionReceivedEvent event) { updateTelemetry(localStorage -> localStorage.fixSuggestionReceived( event.fixSuggestionId(), event.source(), event.snippetsCount(), event.wasGeneratedFromIde())); } @EventListener public void onIssuesRaised(IssuesRaisedEvent event) { var issuesToReport = event.issues().stream() .filter(RaisedIssueDto::isAiCodeFixable) .map(RaisedFindingDto::getId) .collect(Collectors.toSet()); updateTelemetry(localStorage -> localStorage.addIssuesWithPossibleAiFixFromIde(issuesToReport)); } @EventListener public void onCampaignShown(CampaignShownEvent event) { updateTelemetry(localStorage -> localStorage.campaignShown(event.campaignName())); } @EventListener public void onCampaignResolved(CampaignResolvedEvent event) { updateTelemetry(localStorage -> localStorage.campaignResolved(event.campaignName(), event.resolution())); } @PreDestroy public void close() { if ((!MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, 1, TimeUnit.SECONDS)) && (InternalDebug.isEnabled())) { LOG.error("Failed to stop telemetry executor"); } } public void updateListFilesPerformance(int size, long timeMs) { if (!isTelemetryFeatureEnabled) { LOG.info("Telemetry disabled on server startup"); return; } updateTelemetry(localStorage -> localStorage.updateListFilesPerformance(size, timeMs)); } public void supportedLanguagesPanelOpened() { updateTelemetry(TelemetryLocalStorage::incrementSupportedLanguagesPanelOpenedCount); } public void supportedLanguagesPanelCtaClicked() { updateTelemetry(TelemetryLocalStorage::incrementSupportedLanguagesPanelCtaClickedCount); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetrySpringConfig.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.nio.file.Path; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import({ TelemetryService.class, TelemetryManager.class, TelemetryLocalStorageManager.class, TelemetryHttpClient.class, TelemetryServerAttributesProvider.class }) public class TelemetrySpringConfig { public static final String PROPERTY_TELEMETRY_ENDPOINT = "sonarlint.internal.telemetry.endpoint"; private static final String TELEMETRY_ENDPOINT = "https://telemetry.sonarsource.com/sonarlint"; @Bean(name = "telemetryPath") Path provideTelemetryPath(UserPaths userPaths) { return userPaths.getHomeIdeSpecificDir("telemetry").resolve("usage"); } @Bean(name = "telemetryEndpoint") String provideTelemetryEndpoint() { return System.getProperty(PROPERTY_TELEMETRY_ENDPOINT, TELEMETRY_ENDPOINT); } @Bean(name = "initializeParams") InitializeParams provideInitializeParams(InitializeParams params) { return params; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/GessieSpringConfig.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import({ GessieService.class, GessieHttpClient.class }) public class GessieSpringConfig { public static final String PROPERTY_GESSIE_ENDPOINT = "sonarlint.internal.telemetry.gessie.endpoint"; public static final String PROPERTY_GESSIE_API_KEY = "sonarlint.internal.telemetry.gessie.api.key"; private static final String GESSIE_ENDPOINT = "https://events.sonardata.io"; private static final String IDE_SOURCE = "CiiwpdWnR21rWEOkgJ8tr3EYSXb7dzaQ5ezbipLb"; @Bean String gessieEndpoint() { return System.getProperty(PROPERTY_GESSIE_ENDPOINT, GESSIE_ENDPOINT); } @Bean String gessieApiKey() { return System.getProperty(PROPERTY_GESSIE_API_KEY, IDE_SOURCE); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.gessie; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/IntroductionDateProvider.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; public interface IntroductionDateProvider { Instant determineIntroductionDate(Path filePath, Collection lineNumber); } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/IssueMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.time.Instant; import java.util.EnumMap; import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.RawIssue; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; import static org.sonarsource.sonarlint.core.tracking.TextRangeUtils.getLineWithHash; import static org.sonarsource.sonarlint.core.tracking.TextRangeUtils.getTextRangeWithHash; public class IssueMapper { private static final Map STATUS_MAPPING = statusMapping(); private IssueMapper() { // utils } public static TrackedIssue toTrackedIssue(RawIssue issue, Instant introductionDate) { return new TrackedIssue(UUID.randomUUID(), issue.getMessage(), introductionDate, false, issue.getSeverity(), issue.getRuleType(), issue.getRuleKey(), getTextRangeWithHash(issue.getTextRange(), issue.getClientInputFile()), getLineWithHash(issue.getTextRange(), issue.getClientInputFile()), null, issue.getImpacts(), issue.getFlows(), issue.getQuickFixes(), issue.getVulnerabilityProbability(), null, null, issue.getRuleDescriptionContextKey(), issue.getCleanCodeAttribute(), issue.getFileUri()); } public static ResolutionStatus mapStatus(@Nullable IssueStatus status) { return STATUS_MAPPING.get(status); } private static EnumMap statusMapping() { return new EnumMap<>(Map.of( IssueStatus.ACCEPT, ResolutionStatus.ACCEPT, IssueStatus.FALSE_POSITIVE, ResolutionStatus.FALSE_POSITIVE, IssueStatus.WONT_FIX, ResolutionStatus.WONT_FIX )); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/IssueStatusBinding.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.IssueStatus; public class IssueStatusBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return IssueStatus.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final var cPair = (IssueStatus) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80_000_000); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/KnownFindings.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.nio.file.Path; import java.util.List; import java.util.Map; import org.sonarsource.sonarlint.core.commons.KnownFinding; public class KnownFindings { private final Map> issuesPerFile; private final Map> securityHotspotsPerFile; public KnownFindings(Map> issuesPerFile, Map> securityHotspotsPerFile) { this.issuesPerFile = issuesPerFile; this.securityHotspotsPerFile = securityHotspotsPerFile; } public Map> getIssuesPerFile() { return issuesPerFile; } public Map> getSecurityHotspotsPerFile() { return securityHotspotsPerFile; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/LocalOnlyIssueRepository.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; public class LocalOnlyIssueRepository { private final Map> localOnlyIssuesByRelativePath = new ConcurrentHashMap<>(); public void save(Path serverRelativePath, List localOnlyIssues) { localOnlyIssuesByRelativePath.put(serverRelativePath, localOnlyIssues); } public Optional findByKey(UUID localOnlyIssueKey) { return localOnlyIssuesByRelativePath.values().stream().flatMap(List::stream).filter(issue -> issue.getId().equals(localOnlyIssueKey)).findFirst(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/LocalOnlySecurityHotspot.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.util.UUID; public class LocalOnlySecurityHotspot { private final UUID id; public LocalOnlySecurityHotspot(UUID id) { this.id = id; } public UUID getId() { return id; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TaintVulnerabilityTrackingService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.time.Instant; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Predicate; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ServerIssueStatusChangedEvent; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.event.TaintVulnerabilitiesSynchronizedEvent; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.mode.SeverityModeService; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixFeature; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ImpactDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.taint.vulnerability.DidChangeTaintVulnerabilitiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.MQRModeDetails; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rpc.protocol.common.StandardModeDetails; import org.sonarsource.sonarlint.core.serverapi.push.IssueChangedEvent; import org.sonarsource.sonarlint.core.serverapi.push.TaintVulnerabilityClosedEvent; import org.sonarsource.sonarlint.core.serverapi.push.TaintVulnerabilityRaisedEvent; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.TaintSynchronizationService; import org.springframework.context.event.EventListener; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; public class TaintVulnerabilityTrackingService { private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final SonarProjectBranchTrackingService branchTrackingService; private final TaintSynchronizationService taintSynchronizationService; private final StorageService storageService; private final PathTranslationService pathTranslationService; private final SeverityModeService severityModeService; private final AiCodeFixRepository aiCodeFixRepository; public TaintVulnerabilityTrackingService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, SonarProjectBranchTrackingService branchTrackingService, TaintSynchronizationService taintSynchronizationService, StorageService storageService, PathTranslationService pathTranslationService, SeverityModeService severityModeService, AiCodeFixRepository aiCodeFixRepository) { this.client = client; this.configurationRepository = configurationRepository; this.branchTrackingService = branchTrackingService; this.taintSynchronizationService = taintSynchronizationService; this.storageService = storageService; this.pathTranslationService = pathTranslationService; this.severityModeService = severityModeService; this.aiCodeFixRepository = aiCodeFixRepository; } public List listAll(String configurationScopeId, boolean shouldRefresh, SonarLintCancelMonitor cancelMonitor) { return configurationRepository.getEffectiveBinding(configurationScopeId) .map(binding -> loadTaintVulnerabilities(configurationScopeId, binding, shouldRefresh, cancelMonitor)) .orElseGet(Collections::emptyList); } @EventListener public void onServerEventReceived(SonarServerEventReceivedEvent eventReceived) { var connectionId = eventReceived.getConnectionId(); var serverEvent = eventReceived.getEvent(); if (serverEvent instanceof TaintVulnerabilityRaisedEvent raisedEvent) { insertIntoStorageAndNotifyClient(connectionId, raisedEvent); } else if (serverEvent instanceof TaintVulnerabilityClosedEvent closedEvent) { removeFromStorageAndNotifyClient(connectionId, closedEvent); } else if ((serverEvent instanceof IssueChangedEvent changedEvent)) { updateStorageAndNotifyClient(connectionId, changedEvent); } } @EventListener public void onServerIssueStatusChanged(ServerIssueStatusChangedEvent event) { var finding = event.getFinding(); if (finding instanceof ServerTaintIssue taintVulnerability) { var connectionId = event.getConnectionId(); var projectKey = event.getProjectKey(); var isMQRMode = severityModeService.isMQRModeForConnection(event.getConnectionId()); var isAiCodeFixable = isAiCodeFixable(taintVulnerability, new Binding(connectionId, projectKey)); configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey) .forEach(boundScope -> { var newCodeDefinition = storageService.connection(connectionId).project(projectKey).newCodeDefinition().read() .>map(definition -> definition::isOnNewCode).orElse(date -> true); var pathTranslation = pathTranslationService.getOrComputePathTranslation(boundScope.getConfigScopeId()); pathTranslation.ifPresent(translation -> client.didChangeTaintVulnerabilities( new DidChangeTaintVulnerabilitiesParams(boundScope.getConfigScopeId(), emptySet(), emptyList(), List.of(toDto(taintVulnerability, newCodeDefinition, translation, isMQRMode, isAiCodeFixable))))); }); } } @EventListener public void onTaintVulnerabilitiesSynchronized(TaintVulnerabilitiesSynchronizedEvent event) { var summary = event.getSummary(); var connectionId = event.getConnectionId(); var sonarProjectKey = event.getSonarProjectKey(); var newCodeDefinition = storageService.connection(connectionId).project(sonarProjectKey).newCodeDefinition().read() .>map(definition -> definition::isOnNewCode).orElse(date -> true); var isMQRMode = severityModeService.isMQRModeForConnection(event.getConnectionId()); configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, sonarProjectKey).forEach(boundScope -> { var pathTranslation = pathTranslationService.getOrComputePathTranslation(boundScope.getConfigScopeId()); pathTranslation .ifPresent( translation -> client.didChangeTaintVulnerabilities(new DidChangeTaintVulnerabilitiesParams(boundScope.getConfigScopeId(), summary.deletedItemIds(), summary.addedItems().stream() .map(taint -> { var isAiCodeFixable = isAiCodeFixable(taint, new Binding(connectionId, sonarProjectKey)); return toDto(taint, newCodeDefinition, translation, isMQRMode, isAiCodeFixable); }) .toList(), summary.updatedItems().stream() .map(taint -> { var isAiCodeFixable = isAiCodeFixable(taint, new Binding(connectionId, sonarProjectKey)); return toDto(taint, newCodeDefinition, translation, isMQRMode, isAiCodeFixable); }) .toList()))); }); } private void insertIntoStorageAndNotifyClient(String connectionId, TaintVulnerabilityRaisedEvent event) { var newTaintVulnerability = new ServerTaintIssue( UUID.randomUUID(), event.getKey(), false, null, event.getRuleKey(), event.getMainLocation().getMessage(), event.getMainLocation().getFilePath(), event.getCreationDate(), event.getSeverity(), event.getType(), adapt(event.getMainLocation().getTextRange()), event.getRuleDescriptionContextKey(), event.getCleanCodeAttribute().orElse(null), event.getImpacts(), adapt(event.getFlows())); var projectKey = event.getProjectKey(); var binding = new Binding(connectionId, projectKey); storageService.binding(binding).findings().insert(event.getBranchName(), newTaintVulnerability); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); var isAiCodeFixable = isAiCodeFixable(newTaintVulnerability, binding); configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey).forEach(boundScope -> { var pathTranslation = pathTranslationService.getOrComputePathTranslation(boundScope.getConfigScopeId()); pathTranslation.ifPresent(translation -> client.didChangeTaintVulnerabilities( new DidChangeTaintVulnerabilitiesParams(boundScope.getConfigScopeId(), emptySet(), List.of(toDto(newTaintVulnerability, date -> true, translation, isMQRMode, isAiCodeFixable)), emptyList()))); }); } private void removeFromStorageAndNotifyClient(String connectionId, TaintVulnerabilityClosedEvent event) { var projectKey = event.getProjectKey(); storageService.connection(connectionId) .project(projectKey) .findings() .deleteTaintIssueBySonarServerKey(event.getTaintIssueKey()) .ifPresent(deletedId -> configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey).forEach(boundScope -> client .didChangeTaintVulnerabilities(new DidChangeTaintVulnerabilitiesParams(boundScope.getConfigScopeId(), Set.of(deletedId), emptyList(), emptyList())))); } private void updateStorageAndNotifyClient(String connectionId, IssueChangedEvent event) { var projectKey = event.getProjectKey(); var updatedTaintVulnerabilities = updateTaintIssues(connectionId, projectKey, event.getUserSeverity(), event.getUserType(), event.getResolved(), event.getImpactedIssues()); if (!updatedTaintVulnerabilities.isEmpty()) { configurationRepository.getBoundScopesToConnectionAndSonarProject(connectionId, projectKey).forEach(boundScope -> { var newCodeDefinition = storageService.connection(connectionId).project(projectKey).newCodeDefinition().read() .>map(definition -> definition::isOnNewCode).orElse(date -> true); var isMQRMode = severityModeService.isMQRModeForConnection(connectionId); pathTranslationService.getOrComputePathTranslation(boundScope.getConfigScopeId()) .ifPresent(translation -> client.didChangeTaintVulnerabilities(new DidChangeTaintVulnerabilitiesParams(boundScope.getConfigScopeId(), emptySet(), emptyList(), updatedTaintVulnerabilities.stream() .map(taintIssue -> { var isAiCodeFixable = isAiCodeFixable(taintIssue, boundScope.getBinding()); return toDto(taintIssue, newCodeDefinition, translation, isMQRMode, isAiCodeFixable); }) .toList()))); }); } } private List updateTaintIssues(String connectionId, String projectKey, @Nullable org.sonarsource.sonarlint.core.commons.IssueSeverity userSeverity, @Nullable org.sonarsource.sonarlint.core.commons.RuleType userType, @Nullable Boolean resolved, List issues) { var findingsStorage = storageService.connection(connectionId).project(projectKey).findings(); return issues.stream().map(issueEvent -> findingsStorage.updateTaintIssueBySonarServerKey(issueEvent.getIssueKey(), issue -> { if (userSeverity != null) { issue.setSeverity(userSeverity); } if (userType != null) { issue.setType(userType); } if (resolved != null) { issue.setResolved(resolved); } var impacts = issueEvent.getImpacts(); if (!impacts.isEmpty()) { issue.setImpacts(mergeImpacts(issue.getImpacts(), impacts)); } })).flatMap(Optional::stream).toList(); } private static Map mergeImpacts( Map defaultImpacts, Map overriddenImpacts) { var mergedImpacts = new EnumMap(org.sonarsource.sonarlint.core.commons.SoftwareQuality.class); if (!defaultImpacts.isEmpty()) { mergedImpacts = new EnumMap<>(defaultImpacts); } for (var entry : overriddenImpacts.entrySet()) { var quality = org.sonarsource.sonarlint.core.commons.SoftwareQuality.valueOf(entry.getKey().name()); var severity = ImpactSeverity.mapSeverity(entry.getValue().name()); mergedImpacts.put(quality, severity); } return Collections.unmodifiableMap(mergedImpacts); } private List loadTaintVulnerabilities(String configurationScopeId, Binding binding, boolean shouldRefresh, SonarLintCancelMonitor cancelMonitor) { var matchedBranchOpt = branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId); var pathTranslationOpt = pathTranslationService.getOrComputePathTranslation(configurationScopeId); if (matchedBranchOpt.isPresent() && pathTranslationOpt.isPresent()) { if (shouldRefresh) { taintSynchronizationService.synchronizeTaintVulnerabilities(binding.connectionId(), binding.sonarProjectKey(), cancelMonitor); } var projectStorage = storageService.binding(binding); var newCodeDefinition = projectStorage.newCodeDefinition().read().>map(definition -> definition::isOnNewCode).orElse(date -> true); var isMQRMode = severityModeService.isMQRModeForConnection(binding.connectionId()); var translation = pathTranslationOpt.get(); String matchedBranch = matchedBranchOpt.get(); return projectStorage.findings().loadTaint(matchedBranch) .stream().map(serverTaintIssue -> { var isAiCodeFixable = isAiCodeFixable(serverTaintIssue, binding); return toDto(serverTaintIssue, newCodeDefinition, translation, isMQRMode, isAiCodeFixable); }) .toList(); } else { return Collections.emptyList(); } } public Optional getTaintVulnerability(String configurationScopeId, UUID issueId, SonarLintCancelMonitor cancelMonitor) { var maybeBinding = configurationRepository.getEffectiveBinding(configurationScopeId); return maybeBinding.flatMap(binding -> loadTaintVulnerabilities(configurationScopeId, binding, false, cancelMonitor) .stream() .filter(taintVulnerabilityDto -> taintVulnerabilityDto.getId().equals(issueId)) .findFirst()); } private static TaintVulnerabilityDto toDto(ServerTaintIssue serverTaintIssue, Predicate isOnNewCode, FilePathTranslation translation, boolean isMQRMode, boolean isAiCodeFixable) { var cleanCodeAttribute = serverTaintIssue.getCleanCodeAttribute().map(attribute -> CleanCodeAttribute.valueOf(attribute.name())).orElse(null); var impacts = serverTaintIssue.getImpacts().entrySet().stream() .map(e -> new ImpactDto(SoftwareQuality.valueOf(e.getKey().name()), org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity.valueOf(e.getValue().name()))) .toList(); var resolutionStatus = IssueMapper.mapStatus(serverTaintIssue.getResolutionStatus()); return new TaintVulnerabilityDto( serverTaintIssue.getId(), serverTaintIssue.getSonarServerKey(), serverTaintIssue.isResolved(), resolutionStatus, serverTaintIssue.getRuleKey(), serverTaintIssue.getMessage(), translation.serverToIdePath(serverTaintIssue.getFilePath()), serverTaintIssue.getCreationDate(), // In practice, it could happen that the Clean Code Attribute is set but the impacts are empty. isMQRMode && !impacts.isEmpty() && cleanCodeAttribute != null ? Either.forRight(new MQRModeDetails(cleanCodeAttribute, impacts)) : Either.forLeft(new StandardModeDetails(IssueSeverity.valueOf(serverTaintIssue.getSeverity().name()), RuleType.valueOf(serverTaintIssue.getType().name()))), toDto(serverTaintIssue.getFlows(), translation), TextRangeUtils.adapt(serverTaintIssue.getTextRange()), serverTaintIssue.getRuleDescriptionContextKey(), isOnNewCode.test(serverTaintIssue.getCreationDate()), isAiCodeFixable); } private boolean isAiCodeFixable(ServerTaintIssue serverTaintIssue, Binding binding) { return aiCodeFixRepository.get(binding.connectionId()) .map(AiCodeFixService::aiCodeFixMapping) .filter(feature -> feature.isFeatureEnabled(binding.sonarProjectKey())) .map(AiCodeFixFeature::new) .map(feature -> feature.isFixable(serverTaintIssue)) .orElse(false); } public static List toDto(List flows, FilePathTranslation translation) { return flows.stream().map(flow -> new TaintVulnerabilityDto.FlowDto( flow.locations().stream() .map(location -> { var filePath = location.filePath(); return new TaintVulnerabilityDto.FlowDto.LocationDto(TextRangeUtils.adapt(location.textRange()), location.message(), filePath == null ? null : translation.serverToIdePath(filePath)); }) .toList())) .toList(); } private static List adapt(List flows) { return flows.stream().map(TaintVulnerabilityTrackingService::adapt).toList(); } private static ServerTaintIssue.Flow adapt(TaintVulnerabilityRaisedEvent.Flow flow) { return new ServerTaintIssue.Flow(flow.getLocations().stream().map(TaintVulnerabilityTrackingService::adapt).toList()); } private static ServerTaintIssue.ServerIssueLocation adapt(TaintVulnerabilityRaisedEvent.Location location) { return new ServerTaintIssue.ServerIssueLocation( location.getFilePath(), adapt(location.getTextRange()), location.getMessage()); } public static TextRangeWithHash adapt(TaintVulnerabilityRaisedEvent.Location.TextRange range) { return new TextRangeWithHash(range.getStartLine(), range.getStartLineOffset(), range.getEndLine(), range.getEndLineOffset(), range.getHash()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TextRangeUtils.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.io.IOException; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TextRangeWithHashDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto; import static org.apache.commons.lang3.StringUtils.isEmpty; public class TextRangeUtils { private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s"); private TextRangeUtils() { // utils } @CheckForNull public static TextRangeWithHash getTextRangeWithHash(@Nullable TextRange textRange, @Nullable ClientInputFile file) { if (textRange == null) return null; String hash = computeTextRangeHash(textRange, file); return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), hash); } @CheckForNull public static LineWithHash getLineWithHash(@Nullable TextRange textRange, @Nullable ClientInputFile file) { if (textRange == null) return null; String hash = computeLineHash(textRange, file); return new LineWithHash(textRange.getStartLine(), hash); } @CheckForNull public static TextRangeDto toTextRangeDto(@Nullable TextRangeWithHash textRange) { if (textRange == null) return null; return new TextRangeDto(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset()); } @CheckForNull public static TextRangeDto toTextRangeDto(@Nullable TextRange textRange) { if (textRange == null) return null; return new TextRangeDto(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset()); } static String computeTextRangeHash(TextRange textRange, @Nullable ClientInputFile file) { if (file == null) return ""; var textRangeContent = getTextRangeContent(file, textRange); return hash(textRangeContent); } static String computeLineHash(TextRange textRange, @Nullable ClientInputFile file) { if (file == null) return ""; var textRangeContent = getLineContent(file, textRange); return hash(textRangeContent); } private static String getLineContent(ClientInputFile file, TextRange textRange) { var fileContent = getFileContentOrEmptyString(file); if (isEmpty(fileContent)) return ""; var lines = fileContent.lines().toList(); if (lines.size() < textRange.getStartLine()) return ""; var line = lines.get(textRange.getStartLine() - 1); return hash(line); } static String getFileContentOrEmptyString(ClientInputFile file) { try { return file.contents(); } catch (IOException e) { return ""; } } public static String getTextRangeContent(@Nullable ClientInputFile file, @Nullable TextRange textRange) { if (file == null || textRange == null) return ""; var contentLines = getFileContentOrEmptyString(file).lines().toList(); var startLine = textRange.getStartLine() - 1; var endLine = textRange.getEndLine() - 1; if (startLine == endLine) { var startLineContent = contentLines.get(startLine); var endLineOffset = Math.min(textRange.getEndLineOffset(), startLineContent.length()); return startLineContent.substring(textRange.getStartLineOffset(), endLineOffset); } var contentBuilder = new StringBuilder(); contentBuilder.append(contentLines.get(startLine).substring(textRange.getStartLineOffset())) .append(System.lineSeparator()); for (int i = startLine + 1; i < endLine; i++) { contentBuilder.append(contentLines.get(i)).append(System.lineSeparator()); } var endLineContent = endLine < contentLines.size() ? contentLines.get(endLine) : ""; var endLineOffset = Math.min(textRange.getEndLineOffset(), endLineContent.length()); contentBuilder.append(endLineContent, 0, endLineOffset); return contentBuilder.toString(); } @CheckForNull public static TextRangeWithHashDto adapt(@Nullable TextRangeWithHash textRange) { return textRange == null ? null : new TextRangeWithHashDto(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), textRange.getHash()); } static String hash(String codeSnippet) { String codeSnippetWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(codeSnippet).replaceAll(""); return DigestUtils.md5Hex(codeSnippetWithoutWhitespaces); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackedIssue.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.net.URI; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.analysis.api.Flow; import org.sonarsource.sonarlint.core.analysis.api.QuickFix; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; public class TrackedIssue { private final UUID id; private final String message; private final String ruleKey; private final TextRangeWithHash textRangeWithHash; private final LineWithHash lineWithHash; private final String serverKey; private final Instant introductionDate; private final boolean resolved; private final IssueSeverity severity; private final RuleType type; private final Map impacts; private final List flows; private final List quickFixes; private final VulnerabilityProbability vulnerabilityProbability; private final HotspotStatus hotspotStatus; private final ResolutionStatus resolutionStatus; private final String ruleDescriptionContextKey; private final CleanCodeAttribute cleanCodeAttribute; private final URI fileUri; public TrackedIssue(UUID id, String message, @Nullable Instant introductionDate, boolean resolved, IssueSeverity overriddenSeverity, RuleType type, String ruleKey, @Nullable TextRangeWithHash textRangeWithHash, @Nullable LineWithHash lineWithHash, @Nullable String serverKey, Map impacts, List flows, List quickFixes, @Nullable VulnerabilityProbability vulnerabilityProbability, @Nullable HotspotStatus hotspotStatus, @Nullable ResolutionStatus resolutionStatus, @Nullable String ruleDescriptionContextKey, CleanCodeAttribute cleanCodeAttribute, @Nullable URI fileUri) { this.id = id; this.message = message; this.ruleKey = ruleKey; this.textRangeWithHash = textRangeWithHash; this.lineWithHash = lineWithHash; this.serverKey = serverKey; this.introductionDate = introductionDate; this.resolved = resolved; this.severity = overriddenSeverity; this.type = type; this.impacts = impacts; this.flows = flows; this.quickFixes = quickFixes; this.vulnerabilityProbability = vulnerabilityProbability; this.hotspotStatus = hotspotStatus; this.resolutionStatus = resolutionStatus; this.ruleDescriptionContextKey = ruleDescriptionContextKey; this.cleanCodeAttribute = cleanCodeAttribute; this.fileUri = fileUri; } public UUID getId() { return id; } @CheckForNull public String getServerKey() { return serverKey; } public Instant getIntroductionDate() { return introductionDate; } public boolean isResolved() { return resolved; } public IssueSeverity getSeverity() { return severity; } public RuleType getType() { return type; } public boolean isSecurityHotspot() { return getType() == RuleType.SECURITY_HOTSPOT; } public String getRuleKey() { return ruleKey; } @CheckForNull public TextRangeWithHash getTextRangeWithHash() { return textRangeWithHash; } public String getMessage() { return message; } public Map getImpacts() { return impacts; } public List getFlows() { return flows; } public List getQuickFixes() { return quickFixes; } @CheckForNull public VulnerabilityProbability getVulnerabilityProbability() { return vulnerabilityProbability; } @CheckForNull public String getRuleDescriptionContextKey() { return ruleDescriptionContextKey; } public CleanCodeAttribute getCleanCodeAttribute() { return cleanCodeAttribute; } @CheckForNull public LineWithHash getLineWithHash() { return lineWithHash; } @CheckForNull public URI getFileUri() { return fileUri; } @CheckForNull public HotspotStatus getHotspotStatus() { return hotspotStatus; } public ResolutionStatus getResolutionStatus() { return resolutionStatus; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackingService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import jakarta.annotation.PostConstruct; import java.net.URI; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.analysis.AnalysisFailedEvent; import org.sonarsource.sonarlint.core.analysis.AnalysisFinishedEvent; import org.sonarsource.sonarlint.core.analysis.AnalysisStartedEvent; import org.sonarsource.sonarlint.core.analysis.RawIssueDetectedEvent; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.MultiFileBlameResult; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.git.GitService; import org.sonarsource.sonarlint.core.commons.util.git.exceptions.GitException; import org.sonarsource.sonarlint.core.event.MatchingSessionEndedEvent; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.newcode.NewCodeService; import org.sonarsource.sonarlint.core.reporting.FindingReportingService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.GetBaseDirParams; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.issues.KnownFindingsRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.LocalOnlyIssuesRepository; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.FindingsSynchronizationService; import org.sonarsource.sonarlint.core.tracking.matching.IssueMatcher; import org.sonarsource.sonarlint.core.tracking.matching.LocalOnlyIssueMatchingAttributesMapper; import org.sonarsource.sonarlint.core.tracking.matching.MatchingSession; import org.sonarsource.sonarlint.core.tracking.matching.ServerHotspotMatchingAttributesMapper; import org.sonarsource.sonarlint.core.tracking.matching.ServerIssueMatchingAttributesMapper; import org.sonarsource.sonarlint.core.tracking.matching.TrackedIssueFindingMatchingAttributeMapper; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toMap; public class TrackingService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRpcClient client; private final ConfigurationRepository configurationRepository; private final SonarProjectBranchTrackingService branchTrackingService; private final PathTranslationService pathTranslationService; private final FindingReportingService reportingService; private final Map matchingSessionByAnalysisId = new HashMap<>(); private final XodusKnownFindingsStorageService knownFindingsStorageService; private final StorageService storageService; private final LocalOnlyIssueRepository localOnlyIssueRepository; private final FindingsSynchronizationService findingsSynchronizationService; private final NewCodeService newCodeService; private final ApplicationEventPublisher eventPublisher; private final KnownFindingsRepository knownFindingsRepository; private final LocalOnlyIssuesRepository localOnlyIssuesRepository; private final GitService gitService; public TrackingService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, SonarProjectBranchTrackingService branchTrackingService, PathTranslationService pathTranslationService, FindingReportingService reportingService, XodusKnownFindingsStorageService knownFindingsStorageService, StorageService storageService, LocalOnlyIssueRepository localOnlyIssueRepository, FindingsSynchronizationService findingsSynchronizationService, NewCodeService newCodeService, ApplicationEventPublisher eventPublisher, KnownFindingsRepository knownFindingsRepository, LocalOnlyIssuesRepository localOnlyIssuesRepository) { this.client = client; this.configurationRepository = configurationRepository; this.branchTrackingService = branchTrackingService; this.pathTranslationService = pathTranslationService; this.reportingService = reportingService; this.knownFindingsStorageService = knownFindingsStorageService; this.storageService = storageService; this.localOnlyIssueRepository = localOnlyIssueRepository; this.findingsSynchronizationService = findingsSynchronizationService; this.newCodeService = newCodeService; this.eventPublisher = eventPublisher; this.knownFindingsRepository = knownFindingsRepository; this.localOnlyIssuesRepository = localOnlyIssuesRepository; this.gitService = GitService.create(); } @PostConstruct public void migrateData() { if (knownFindingsStorageService.exists()) { try { LOG.info("Migrating the Xodus known findings to H2"); var migrationStart = System.currentTimeMillis(); var xodusKnownFindingsStore = knownFindingsStorageService.get(); var findingsPerConfigScope = xodusKnownFindingsStore.loadAll(); knownFindingsRepository.storeFindings(findingsPerConfigScope); LOG.info("Migrated Xodus known findings to H2, took {}ms", System.currentTimeMillis() - migrationStart); } catch (Exception e) { LOG.error("Unable to migrate known findings, will use fresh DB", e); } } // always call to remove lingering temporary files knownFindingsStorageService.delete(); } @EventListener public void onAnalysisStarted(AnalysisStartedEvent event) { var configurationScopeId = event.getConfigurationScopeId(); var matchingSession = startMatchingSession(configurationScopeId, event.getFileRelativePaths(), event.getFileUris(), event.getFileContentProvider()); matchingSessionByAnalysisId.put(event.getAnalysisId(), matchingSession); reportingService.resetFindingsForFiles(configurationScopeId, event.getFileUris()); reportingService.initFilesToAnalyze(event.getAnalysisId(), event.getFileUris()); } @EventListener public void onIssueDetected(RawIssueDetectedEvent event) { var analysisId = event.analysisId(); var matchingSession = matchingSessionByAnalysisId.get(analysisId); if (matchingSession == null) { // an issue was detected outside any analysis, this normally shouldn't happen return; } var detectedIssue = event.detectedIssue(); var isSupported = detectedIssue.isInFile(); if (isSupported) { // we don't support global issues for now var trackedIssue = matchingSession.matchWithKnownFinding(requireNonNull(detectedIssue.getIdeRelativePath()), detectedIssue); reportingService.streamIssue(event.configurationScopeId(), analysisId, trackedIssue); } } @EventListener public void onAnalysisFailed(AnalysisFailedEvent event) { matchingSessionByAnalysisId.remove(event.analysisId()); } @EventListener public void onAnalysisFinished(AnalysisFinishedEvent event) { var analysisId = event.getAnalysisId(); var matchingSession = matchingSessionByAnalysisId.remove(analysisId); if (matchingSession == null) { // a not-started analysis finished, this normally shouldn't happen return; } var configurationScopeId = event.getConfigurationScopeId(); if (event.shouldFetchServerIssues()) { findingsSynchronizationService.refreshServerFindings(configurationScopeId, matchingSession.getRelativePathsInvolved()); } var result = matchWithServerFindings(configurationScopeId, matchingSession); reportingService.reportTrackedFindings(configurationScopeId, analysisId, result.issuesToReport, result.hotspotsToReport); } private MatchingResult matchWithServerFindings(String configurationScopeId, MatchingSession matchingSession) { var effectiveBindingOpt = configurationRepository.getEffectiveBinding(configurationScopeId); var activeBranchOpt = branchTrackingService.awaitEffectiveSonarProjectBranch(configurationScopeId); var translationOpt = pathTranslationService.getOrComputePathTranslation(configurationScopeId); var issuesToReport = matchingSession.getIssuesPerFile(); var hotspotsToReport = matchingSession.getSecurityHotspotsPerFile(); if (effectiveBindingOpt.isPresent() && activeBranchOpt.isPresent() && translationOpt.isPresent()) { var binding = effectiveBindingOpt.get(); var activeBranch = activeBranchOpt.get(); var translation = translationOpt.get(); issuesToReport = issuesToReport.entrySet().stream().map(e -> { var ideRelativePath = e.getKey(); var serverRelativePath = translation.ideToServerPath(ideRelativePath); var serverIssues = storageService.binding(binding).findings().load(activeBranch, serverRelativePath); var localOnlyIssues = localOnlyIssuesRepository.loadForFile(configurationScopeId, serverRelativePath); var matches = matchWithServerIssues(serverRelativePath, serverIssues, localOnlyIssues, e.getValue()); return Map.entry(ideRelativePath, matches); }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); hotspotsToReport = hotspotsToReport.entrySet().stream().map(e -> { var ideRelativePath = e.getKey(); var serverRelativePath = translation.ideToServerPath(ideRelativePath); var serverHotspots = storageService.binding(binding).findings().loadHotspots(activeBranch, serverRelativePath); var matches = matchWithServerHotspots(serverHotspots, e.getValue()); return Map.entry(ideRelativePath, matches); }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } issuesToReport.forEach((clientRelativePath, trackedIssues) -> storeTrackedIssues(configurationScopeId, clientRelativePath, trackedIssues)); hotspotsToReport.forEach((clientRelativePath, trackedHotspots) -> storeTrackedSecurityHotspots(configurationScopeId, clientRelativePath, trackedHotspots)); eventPublisher.publishEvent(new MatchingSessionEndedEvent(matchingSession.countNewIssues(), matchingSession.countRemainingUnmatchedIssues())); return new MatchingResult(issuesToReport, hotspotsToReport); } private void storeTrackedIssues(String configurationScopeId, Path clientRelativePath, Collection newKnownIssues) { knownFindingsRepository.storeKnownIssues(configurationScopeId, clientRelativePath, newKnownIssues.stream().map(i -> new KnownFinding(i.getId(), i.getServerKey(), i.getTextRangeWithHash(), i.getLineWithHash(), i.getRuleKey(), i.getMessage(), i.getIntroductionDate())).toList()); } private void storeTrackedSecurityHotspots(String configurationScopeId, Path clientRelativePath, Collection newKnownSecurityHotspots) { knownFindingsRepository.storeKnownSecurityHotspots(configurationScopeId, clientRelativePath, newKnownSecurityHotspots.stream().map(i -> new KnownFinding(i.getId(), i.getServerKey(), i.getTextRangeWithHash(), i.getLineWithHash(), i.getRuleKey(), i.getMessage(), i.getIntroductionDate())).toList()); } private List matchWithServerIssues(Path serverRelativePath, List> serverIssues, List localOnlyIssues, Collection trackedIssues) { var serverIssueMatcher = new IssueMatcher>(new ServerIssueMatchingAttributesMapper(), serverIssues); var serverMatchingResult = serverIssueMatcher.matchWith(new TrackedIssueFindingMatchingAttributeMapper(), trackedIssues); var localIssueMatcher = new IssueMatcher(new LocalOnlyIssueMatchingAttributesMapper(), localOnlyIssues); var localMatchingResult = localIssueMatcher.matchWith(new TrackedIssueFindingMatchingAttributeMapper(), trackedIssues); var matches = trackedIssues.stream().map(trackedIssue -> { var matchToServer = serverMatchingResult.getMatch(trackedIssue); if (matchToServer != null) { return updateTrackedIssueWithServerData(trackedIssue, matchToServer); } else { var matchToLocal = localMatchingResult.getMatch(trackedIssue); if (matchToLocal != null) { return updateTrackedIssueWithLocalOnlyIssueData(trackedIssue, matchToLocal); } return trackedIssue; } }).toList(); localOnlyIssueRepository.save(serverRelativePath, matches.stream().filter(issue -> issue.getServerKey() == null).map(issue -> newLocalOnlyIssue(serverRelativePath, issue)).toList()); return matches; } private static List matchWithServerHotspots(Collection serverHotspots, Collection trackedIssues) { var serverIssueMatcher = new IssueMatcher(new ServerHotspotMatchingAttributesMapper(), serverHotspots); var serverMatchingResult = serverIssueMatcher.matchWith(new TrackedIssueFindingMatchingAttributeMapper(), trackedIssues); return trackedIssues.stream().map(trackedIssue -> { var matchToServer = serverMatchingResult.getMatch(trackedIssue); if (matchToServer != null) { return updateRawHotspotWithServerData(trackedIssue, matchToServer); } else { return trackedIssue; } }).toList(); } private static LocalOnlyIssue newLocalOnlyIssue(Path serverRelativePath, TrackedIssue issue) { return new LocalOnlyIssue(issue.getId(), serverRelativePath, issue.getTextRangeWithHash(), issue.getLineWithHash(), issue.getRuleKey(), issue.getMessage(), null); } private static TrackedIssue updateTrackedIssueWithServerData(TrackedIssue trackedIssue, ServerIssue serverIssue) { var serverSeverity = serverIssue.getUserSeverity(); var severity = serverSeverity != null ? serverSeverity : trackedIssue.getSeverity(); var impacts = serverIssue.getImpacts().isEmpty() ? trackedIssue.getImpacts() : serverIssue.getImpacts(); var status = IssueMapper.mapStatus(serverIssue.getResolutionStatus()); return new TrackedIssue(trackedIssue.getId(), trackedIssue.getMessage(), serverIssue.getCreationDate(), serverIssue.isResolved(), severity, serverIssue.getType(), serverIssue.getRuleKey(), trackedIssue.getTextRangeWithHash(), trackedIssue.getLineWithHash(), serverIssue.getKey(), impacts, trackedIssue.getFlows(), trackedIssue.getQuickFixes(), trackedIssue.getVulnerabilityProbability(), trackedIssue.getHotspotStatus(), status, trackedIssue.getRuleDescriptionContextKey(), trackedIssue.getCleanCodeAttribute(), trackedIssue.getFileUri()); } private static TrackedIssue updateRawHotspotWithServerData(TrackedIssue trackedHotspot, ServerHotspot serverHotspot) { return new TrackedIssue(trackedHotspot.getId(), trackedHotspot.getMessage(), serverHotspot.getCreationDate(), serverHotspot.getStatus().isResolved(), trackedHotspot.getSeverity(), RuleType.SECURITY_HOTSPOT, trackedHotspot.getRuleKey(), trackedHotspot.getTextRangeWithHash(), trackedHotspot.getLineWithHash(), serverHotspot.getKey(), trackedHotspot.getImpacts(), trackedHotspot.getFlows(), trackedHotspot.getQuickFixes(), serverHotspot.getVulnerabilityProbability(), HotspotStatus.valueOf(serverHotspot.getStatus().name()), null, trackedHotspot.getRuleDescriptionContextKey(), trackedHotspot.getCleanCodeAttribute(), trackedHotspot.getFileUri()); } private static TrackedIssue updateTrackedIssueWithLocalOnlyIssueData(TrackedIssue trackedIssue, LocalOnlyIssue localOnlyIssue) { var resolution = localOnlyIssue.getResolution(); ResolutionStatus status = null; if (resolution != null) { status = IssueMapper.mapStatus(resolution.getStatus()); } return new TrackedIssue(trackedIssue.getId(), trackedIssue.getMessage(), trackedIssue.getIntroductionDate(), resolution != null, trackedIssue.getSeverity(), trackedIssue.getType(), trackedIssue.getRuleKey(), trackedIssue.getTextRangeWithHash(), trackedIssue.getLineWithHash(), trackedIssue.getServerKey(), trackedIssue.getImpacts(), trackedIssue.getFlows(), trackedIssue.getQuickFixes(), trackedIssue.getVulnerabilityProbability(), trackedIssue.getHotspotStatus(), status, trackedIssue.getRuleDescriptionContextKey(), trackedIssue.getCleanCodeAttribute(), trackedIssue.getFileUri()); } private MatchingSession startMatchingSession(String configurationScopeId, Set fileRelativePaths, Set fileUris, UnaryOperator fileContentProvider) { var issuesByRelativePath = fileRelativePaths.stream() .collect(toMap(Function.identity(), relativePath -> knownFindingsRepository.loadIssuesForFile(configurationScopeId, relativePath))); var hotspotsByRelativePath = fileRelativePaths.stream() .collect(toMap(Function.identity(), relativePath -> knownFindingsRepository.loadSecurityHotspotsForFile(configurationScopeId, relativePath))); var introductionDateProvider = getIntroductionDateProvider(configurationScopeId, fileRelativePaths, fileUris, fileContentProvider); var previousFindings = new KnownFindings(issuesByRelativePath, hotspotsByRelativePath); return new MatchingSession(previousFindings, introductionDateProvider); } private IntroductionDateProvider getIntroductionDateProvider(String configurationScopeId, Set fileRelativePaths, Set fileUris, UnaryOperator fileContentProvider) { var baseDir = getBaseDir(configurationScopeId); if (baseDir != null) { try { var newCodeDefinition = newCodeService.getFullNewCodeDefinition(configurationScopeId); var thresholdDate = newCodeDefinition.map(NewCodeDefinition::getThresholdDate).orElse(NewCodeDefinition.withAlwaysNew().getThresholdDate()); var blameResult = gitService.getBlameResult(baseDir, fileRelativePaths, fileUris, fileContentProvider, thresholdDate); return (filePath, lineNumbers) -> determineIntroductionDate(filePath, lineNumbers, blameResult); } catch (GitException e) { LOG.info("Could not get git blame data for file {} in {}. ", e.getPath(), configurationScopeId); } catch (Exception e) { LOG.error("Cannot access blame info for " + configurationScopeId, e); } } LOG.debug("Git blame is not working. Falling back to detection date as the introduction date"); // we keep the detection date as the introduction date return (filePath, lineNumber) -> Instant.now(); } @CheckForNull private Path getBaseDir(String configurationScopeId) { try { return client.getBaseDir(new GetBaseDirParams(configurationScopeId)).join().getBaseDir(); } catch (Exception e) { LOG.error("Error when requesting the base dir", e); return null; } } private static Instant determineIntroductionDate(Path path, Collection lineNumbers, MultiFileBlameResult multiFileBlameResult) { return multiFileBlameResult.getLatestChangeDateForLinesInFile(path, lineNumbers).orElse(Instant.now()); } private record MatchingResult(Map> issuesToReport, Map> hotspotsToReport) { } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/XodusKnownFindingsStorageService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.PreDestroy; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.UserPaths; import static org.sonarsource.sonarlint.core.commons.storage.XodusPurgeUtils.deleteInFolderWithPattern; import static org.sonarsource.sonarlint.core.tracking.XodusKnownFindingsStore.BACKUP_TAR_GZ; import static org.sonarsource.sonarlint.core.tracking.XodusKnownFindingsStore.KNOWN_FINDINGS_STORE; public class XodusKnownFindingsStorageService { private final Path projectsStorageBaseDir; private final Path workDir; private final AtomicReference trackedIssuesStore = new AtomicReference<>(); public XodusKnownFindingsStorageService(UserPaths userPaths) { this.projectsStorageBaseDir = userPaths.getStorageRoot(); this.workDir = userPaths.getWorkDir(); } public boolean exists() { return Files.exists(projectsStorageBaseDir.resolve(XodusKnownFindingsStore.BACKUP_TAR_GZ)); } public synchronized XodusKnownFindingsStore get() { var store = trackedIssuesStore.get(); if (store == null) { try { store = new XodusKnownFindingsStore(projectsStorageBaseDir, workDir); trackedIssuesStore.set(store); return store; } catch (IOException e) { throw new IllegalStateException("Unable to create tracked issues database", e); } } return store; } @PreDestroy public void close() { var store = trackedIssuesStore.get(); if (store != null) { store.close(); } } public void delete() { var store = trackedIssuesStore.getAndSet(null); if (store != null) { store.close(); } FileUtils.deleteQuietly(projectsStorageBaseDir.resolve(BACKUP_TAR_GZ).toFile()); deleteInFolderWithPattern(workDir, KNOWN_FINDINGS_STORE + "*"); deleteInFolderWithPattern(projectsStorageBaseDir, "known_findings_backup*"); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/XodusKnownFindingsStore.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.Map; import java.util.UUID; import java.util.stream.StreamSupport; import jetbrains.exodus.entitystore.Entity; import jetbrains.exodus.entitystore.PersistentEntityStore; import jetbrains.exodus.entitystore.PersistentEntityStores; import jetbrains.exodus.env.EnvironmentConfig; import jetbrains.exodus.env.Environments; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.issues.Findings; import org.sonarsource.sonarlint.core.serverconnection.storage.InstantBinding; import org.sonarsource.sonarlint.core.serverconnection.storage.TarGzUtils; import org.sonarsource.sonarlint.core.serverconnection.storage.UuidBinding; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.flatMapping; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toMap; public class XodusKnownFindingsStore { static final String KNOWN_FINDINGS_STORE = "known-findings-store"; private static final String CONFIGURATION_SCOPE_ID_ENTITY_TYPE = "Scope"; private static final String CONFIGURATION_SCOPE_ID_TO_FILES_LINK_NAME = "files"; private static final String PATH_PROPERTY_NAME = "path"; private static final String NAME_PROPERTY_NAME = "name"; private static final String FILE_TO_ISSUES_LINK_NAME = "issues"; private static final String FILE_TO_SECURITY_HOTSPOTS_LINK_NAME = "hotspots"; private static final String UUID_PROPERTY_NAME = "uuid"; private static final String SERVER_KEY_PROPERTY_NAME = "serverKey"; private static final String INTRODUCTION_DATE_PROPERTY_NAME = "introductionDate"; private static final String RULE_KEY_PROPERTY_NAME = "ruleKey"; private static final String RANGE_HASH_PROPERTY_NAME = "rangeHash"; private static final String LINE_HASH_PROPERTY_NAME = "lineHash"; private static final String START_LINE_PROPERTY_NAME = "startLine"; private static final String START_LINE_OFFSET_PROPERTY_NAME = "startLineOffset"; private static final String END_LINE_PROPERTY_NAME = "endLine"; private static final String END_LINE_OFFSET_PROPERTY_NAME = "endLineOffset"; private static final String MESSAGE_BLOB_NAME = "message"; static final String BACKUP_TAR_GZ = "known_findings_backup.tar.gz"; private final PersistentEntityStore entityStore; private final Path xodusDbDir; private static final SonarLintLogger LOG = SonarLintLogger.get(); public XodusKnownFindingsStore(Path backupDir, Path workDir) throws IOException { xodusDbDir = Files.createTempDirectory(workDir, KNOWN_FINDINGS_STORE); var backupFile = backupDir.resolve(BACKUP_TAR_GZ); if (Files.isRegularFile(backupFile)) { LOG.debug("Restoring previous known findings database from {}", backupFile); try { TarGzUtils.extractTarGz(backupFile, xodusDbDir); } catch (Exception e) { LOG.error("Unable to restore known findings backup {}", backupFile); } } LOG.debug("Starting known findings database from {}", xodusDbDir); this.entityStore = buildEntityStore(); entityStore.executeInTransaction(txn -> { entityStore.registerCustomPropertyType(txn, Instant.class, new InstantBinding()); entityStore.registerCustomPropertyType(txn, UUID.class, new UuidBinding()); }); } public Map> loadAll() { return entityStore.computeInReadonlyTransaction(txn -> StreamSupport.stream(txn.getAll(CONFIGURATION_SCOPE_ID_ENTITY_TYPE).spliterator(), false) .collect(groupingBy( e -> (String) requireNonNull(e.getProperty(NAME_PROPERTY_NAME)), flatMapping(e -> StreamSupport.stream(e.getLinks(CONFIGURATION_SCOPE_ID_TO_FILES_LINK_NAME).spliterator(), false), toMap( f -> Paths.get((String) requireNonNull(f.getProperty(PATH_PROPERTY_NAME))), f -> new Findings( StreamSupport.stream(f.getLinks(FILE_TO_ISSUES_LINK_NAME).spliterator(), false) .map(XodusKnownFindingsStore::adapt).toList(), StreamSupport.stream(f.getLinks(FILE_TO_SECURITY_HOTSPOTS_LINK_NAME).spliterator(), false) .map(XodusKnownFindingsStore::adapt).toList()), Findings::mergeWith))))); } private static KnownFinding adapt(Entity storedFinding) { var uuid = (UUID) requireNonNull(storedFinding.getProperty(UUID_PROPERTY_NAME)); var serverKey = (String) storedFinding.getProperty(SERVER_KEY_PROPERTY_NAME); var introductionDate = (Instant) requireNonNull(storedFinding.getProperty(INTRODUCTION_DATE_PROPERTY_NAME)); var ruleKey = (String) requireNonNull(storedFinding.getProperty(RULE_KEY_PROPERTY_NAME)); var msg = requireNonNull(storedFinding.getBlobString(MESSAGE_BLOB_NAME)); var startLine = (Integer) storedFinding.getProperty(START_LINE_PROPERTY_NAME); TextRangeWithHash textRange = null; LineWithHash lineWithHash = null; if (startLine != null) { var rangeHash = (String) storedFinding.getProperty(RANGE_HASH_PROPERTY_NAME); if (rangeHash != null) { var startLineOffset = (Integer) storedFinding.getProperty(START_LINE_OFFSET_PROPERTY_NAME); var endLine = (Integer) storedFinding.getProperty(END_LINE_PROPERTY_NAME); var endLineOffset = (Integer) storedFinding.getProperty(END_LINE_OFFSET_PROPERTY_NAME); textRange = new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, rangeHash); } var lineHash = (String) storedFinding.getProperty(LINE_HASH_PROPERTY_NAME); if (lineHash != null) { lineWithHash = new LineWithHash(startLine, lineHash); } } return new KnownFinding( uuid, serverKey, textRange, lineWithHash, ruleKey, msg, introductionDate); } private PersistentEntityStore buildEntityStore() { var environment = Environments.newInstance(xodusDbDir.toAbsolutePath().toFile(), new EnvironmentConfig() .setLogAllowRemote(true) .setLogAllowRemovable(true) .setLogAllowRamDisk(true)); var entityStoreImpl = PersistentEntityStores.newInstance(environment); entityStoreImpl.setCloseEnvironment(true); return entityStoreImpl; } public void close() { entityStore.close(); FileUtils.deleteQuietly(xodusDbDir.toFile()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/IssueMatcher.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; /** * Match a collection of issues. * * @param type of the issues that are in the first collection * @param type of the issues that are in the second collection */ public class IssueMatcher { private static final List MATCHING_CRITERIA = List.of( // 1. match issues with same server issue key ServerIssueMatchingCriterion::new, // 2. match issues with same rule, same line and same text range hash, but not necessarily with same message LineAndTextRangeHashMatchingCriterion::new, // 3. match issues with same rule, same message and same text range hash TextRangeHashAndMessageMatchingCriterion::new, // 4. match issues with same rule, same line and same message LineAndMessageMatchingCriterion::new, // 5. match issues with same rule and same text range hash but different line and different message. // See SONAR-2812 TextRangeHashMatchingCriterion::new, // 6. match issues with same rule, same line and same line hash LineAndLineHashMatchingCriterion::new, // 7. match issues with same rule and same line hash LineHashMatchingCriterion::new); private final Map>> rightIssuesByCriterion = new HashMap<>(); private final MatchingAttributesMapper rightMapper; private final Collection rightIssues; public IssueMatcher(MatchingAttributesMapper rightMapper, Collection rightIssues) { this.rightMapper = rightMapper; this.rightIssues = new ArrayList<>(rightIssues); for (var matchingCriterion : MATCHING_CRITERIA) { var issuesByCriterion = new HashMap>(); for (RIGHT right : rightIssues) { var criterionAppliedToIssue = matchingCriterion.build(right, rightMapper); issuesByCriterion.computeIfAbsent(criterionAppliedToIssue, k -> new ArrayList<>()).add(right); } rightIssuesByCriterion.put(matchingCriterion, issuesByCriterion); } } public MatchingResult matchWith(MatchingAttributesMapper leftMapper, Collection leftIssues) { var result = new MatchingResult(leftIssues); for (var matchingCriterion : MATCHING_CRITERIA) { if (result.isComplete()) { break; } matchWithCriterion(result, leftMapper, matchingCriterion); } return result; } private void matchWithCriterion(MatchingResult result, MatchingAttributesMapper leftMapper, MatchingCriterionFactory criterionFactory) { for (LEFT left : result.getUnmatchedLefts()) { var leftKey = criterionFactory.build(left, leftMapper); var rightCandidates = rightIssuesByCriterion.get(criterionFactory).get(leftKey); if (rightCandidates != null && !rightCandidates.isEmpty()) { // TODO taking the first one. Could be improved if there are more than 2 issues on the same line. // Message could be checked to take the best one. var match = rightCandidates.iterator().next(); result.recordMatch(left, match); removeRight(match); } } } private void removeRight(RIGHT right) { rightIssues.remove(right); MATCHING_CRITERIA.forEach(criterion -> { var rights = rightIssuesByCriterion.get(criterion).get(criterion.build(right, rightMapper)); if (rights != null) { rights.remove(right); } }); } public int getUnmatchedIssuesCount() { return rightIssues.size(); } private interface MatchingCriterion { } private interface MatchingCriterionFactory { MatchingCriterion build(G issue, MatchingAttributesMapper mapper); } private static class LineAndTextRangeHashMatchingCriterion implements MatchingCriterion { private final String ruleKey; @Nullable private final String textRangeHash; @Nullable private final Integer line; LineAndTextRangeHashMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.line = mapper.getLine(issue).orElse(null); this.textRangeHash = mapper.getTextRangeHash(issue).orElse(null); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (LineAndTextRangeHashMatchingCriterion) o; // start with most discriminant field return Objects.equals(line, that.line) && Objects.equals(textRangeHash, that.textRangeHash) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + (textRangeHash != null ? textRangeHash.hashCode() : 0); result = 31 * result + (line != null ? line.hashCode() : 0); return result; } } private static class LineAndLineHashMatchingCriterion implements MatchingCriterion { private final String ruleKey; @Nullable private final Integer line; private final String lineHash; LineAndLineHashMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.line = mapper.getLine(issue).orElse(null); this.lineHash = mapper.getLineHash(issue).orElse(""); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (LineAndLineHashMatchingCriterion) o; // start with most discriminant field return Objects.equals(line, that.line) && Objects.equals(lineHash, that.lineHash) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + (lineHash != null ? lineHash.hashCode() : 0); result = 31 * result + (line != null ? line.hashCode() : 0); return result; } } private static class LineHashMatchingCriterion implements MatchingCriterion { private final String ruleKey; private final String lineHash; LineHashMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.lineHash = mapper.getLineHash(issue).orElse(""); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (LineHashMatchingCriterion) o; // start with most discriminant field return Objects.equals(lineHash, that.lineHash) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + (lineHash != null ? lineHash.hashCode() : 0); return result; } } private static class TextRangeHashAndMessageMatchingCriterion implements MatchingCriterion { private final String ruleKey; private final String message; private final String textRangeHash; TextRangeHashAndMessageMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.message = mapper.getMessage(issue); this.textRangeHash = mapper.getTextRangeHash(issue).orElse(null); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (TextRangeHashAndMessageMatchingCriterion) o; // start with most discriminant field return Objects.equals(textRangeHash, that.textRangeHash) && message.equals(that.message) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + message.hashCode(); result = 31 * result + (textRangeHash != null ? textRangeHash.hashCode() : 0); return result; } } private static class LineAndMessageMatchingCriterion implements MatchingCriterion { private final String ruleKey; private final String message; @Nullable private final Integer line; LineAndMessageMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.message = mapper.getMessage(issue); this.line = mapper.getLine(issue).orElse(null); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (LineAndMessageMatchingCriterion) o; // start with most discriminant field return Objects.equals(line, that.line) && message.equals(that.message) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + message.hashCode(); result = 31 * result + (line != null ? line.hashCode() : 0); return result; } } private static class TextRangeHashMatchingCriterion implements MatchingCriterion { private final String ruleKey; private final String textRangeHash; TextRangeHashMatchingCriterion(G issue, MatchingAttributesMapper mapper) { this.ruleKey = mapper.getRuleKey(issue); this.textRangeHash = mapper.getTextRangeHash(issue).orElse(""); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (TextRangeHashMatchingCriterion) o; // start with most discriminant field return Objects.equals(textRangeHash, that.textRangeHash) && ruleKey.equals(that.ruleKey); } @Override public int hashCode() { var result = ruleKey.hashCode(); result = 31 * result + (textRangeHash != null ? textRangeHash.hashCode() : 0); return result; } } private static class ServerIssueMatchingCriterion implements MatchingCriterion { @Nullable private final String serverIssueKey; ServerIssueMatchingCriterion(G issue, MatchingAttributesMapper mapper) { serverIssueKey = mapper.getServerIssueKey(issue).orElse(null); } // note: the design of the enclosing caller ensures that 'o' is of the correct class and not null @Override public boolean equals(Object o) { var that = (ServerIssueMatchingCriterion) o; return that != null && !isBlank(serverIssueKey) && !isBlank(that.serverIssueKey) && serverIssueKey.equals(that.serverIssueKey); } private static boolean isBlank(@Nullable String s) { return s == null || s.isEmpty(); } @Override public int hashCode() { return serverIssueKey != null ? serverIssueKey.hashCode() : 0; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/KnownIssueMatchingAttributesMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class KnownIssueMatchingAttributesMapper implements MatchingAttributesMapper { @Override public String getRuleKey(KnownFinding issue) { return issue.getRuleKey(); } @Override public Optional getLine(KnownFinding issue) { return Optional.ofNullable(issue.getLineWithHash()).map(LineWithHash::getNumber); } @Override public Optional getTextRangeHash(KnownFinding issue) { return Optional.ofNullable(issue.getTextRangeWithHash()).map(TextRangeWithHash::getHash); } @Override public Optional getLineHash(KnownFinding issue) { return Optional.ofNullable(issue.getLineWithHash()).map(LineWithHash::getHash); } @Override public String getMessage(KnownFinding issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(KnownFinding issue) { return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/LocalOnlyIssueMatchingAttributesMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class LocalOnlyIssueMatchingAttributesMapper implements MatchingAttributesMapper { @Override public String getRuleKey(LocalOnlyIssue issue) { return issue.getRuleKey(); } @Override public Optional getLine(LocalOnlyIssue issue) { return Optional.ofNullable(issue.getLineWithHash()).map(LineWithHash::getNumber); } @Override public Optional getTextRangeHash(LocalOnlyIssue issue) { return Optional.ofNullable(issue.getTextRangeWithHash()).map(TextRangeWithHash::getHash); } @Override public Optional getLineHash(LocalOnlyIssue issue) { return Optional.ofNullable(issue.getLineWithHash()).map(LineWithHash::getHash); } @Override public String getMessage(LocalOnlyIssue issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(LocalOnlyIssue issue) { return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/MatchingAttributesMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; public interface MatchingAttributesMapper { String getRuleKey(G issue); Optional getLine(G issue); Optional getTextRangeHash(G issue); Optional getLineHash(G issue); String getMessage(G issue); Optional getServerIssueKey(G issue); } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/MatchingResult.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.CheckForNull; /** * Store the result of matching of issues. * * @param type of the issues that are in the first collection * @param type of the issues that are in the second collection */ public class MatchingResult { /** * Matched issues -> a left issue is associated to a right issue */ private final IdentityHashMap leftToRight = new IdentityHashMap<>(); private final Collection lefts; public MatchingResult(Collection leftIssues) { this.lefts = leftIssues; } /** * Returns an Iterable to be traversed when matching issues. That means * that the traversal does not fail if method {@link #recordMatch(LEFT, RIGHT)} * is called. */ public Iterable getUnmatchedLefts() { List result = new ArrayList<>(); for (LEFT left : lefts) { if (!leftToRight.containsKey(left)) { result.add(left); } } return result; } public Map getMatchedLefts() { return leftToRight; } void recordMatch(LEFT left, RIGHT right) { leftToRight.put(left, right); } boolean isComplete() { return leftToRight.size() == lefts.size(); } @CheckForNull public RIGHT getMatch(LEFT left) { return leftToRight.get(left); } public Optional getMatchOpt(LEFT left) { return Optional.ofNullable(getMatch(left)); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/MatchingSession.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.analysis.RawIssue; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.tracking.IntroductionDateProvider; import org.sonarsource.sonarlint.core.tracking.IssueMapper; import org.sonarsource.sonarlint.core.tracking.KnownFindings; import org.sonarsource.sonarlint.core.tracking.TextRangeUtils; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; public class MatchingSession { private final Map> issueMatchersByFile = new HashMap<>(); private final Map> hotspotMatchersByFile = new HashMap<>(); private final IntroductionDateProvider introductionDateProvider; private final ConcurrentHashMap> issuesPerFile = new ConcurrentHashMap<>(); private final ConcurrentHashMap> securityHotspotsPerFile = new ConcurrentHashMap<>(); private final Set relativePathsInvolved = new HashSet<>(); private long newIssuesFound = 0; public MatchingSession(KnownFindings previousFindings, IntroductionDateProvider introductionDateProvider) { var knownIssuesPerFile = previousFindings.getIssuesPerFile().entrySet().stream().map(entry -> Map.entry(entry.getKey(), new ArrayList<>(entry.getValue()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); var knownSecurityHotspotsPerFile = previousFindings.getSecurityHotspotsPerFile().entrySet().stream() .map(entry -> Map.entry(entry.getKey(), new ArrayList<>(entry.getValue()))).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); knownIssuesPerFile.forEach((path, issues) -> issueMatchersByFile.put(path, new IssueMatcher<>(new KnownIssueMatchingAttributesMapper(), issues))); knownSecurityHotspotsPerFile.forEach((path, hotspots) -> hotspotMatchersByFile.put(path, new IssueMatcher<>(new KnownIssueMatchingAttributesMapper(), hotspots))); this.introductionDateProvider = introductionDateProvider; } public TrackedIssue matchWithKnownFinding(Path relativePath, RawIssue rawIssue) { if (rawIssue.isSecurityHotspot()) { return matchWithKnownSecurityHotspot(relativePath, rawIssue); } else { return matchWithKnownIssue(relativePath, rawIssue); } } public TrackedIssue matchWithKnownSecurityHotspot(Path relativePath, RawIssue newSecurityHotspot) { var hotspotMatcher = hotspotMatchersByFile.get(relativePath); if (hotspotMatcher == null) { throw new IllegalStateException("No hotspot matcher found for " + relativePath); } var trackedSecurityHotspot = matchWithKnownFinding(relativePath, hotspotMatcher, newSecurityHotspot); securityHotspotsPerFile.computeIfAbsent(relativePath, f -> new ArrayList<>()).add(trackedSecurityHotspot); relativePathsInvolved.add(relativePath); return trackedSecurityHotspot; } private TrackedIssue matchWithKnownIssue(Path relativePath, RawIssue rawIssue) { var issueMatcher = issueMatchersByFile.get(relativePath); if (issueMatcher == null) { throw new IllegalStateException("No issue matcher found for " + relativePath); } var trackedIssue = matchWithKnownFinding(relativePath, issueMatcher, rawIssue); issuesPerFile.computeIfAbsent(relativePath, f -> new ArrayList<>()).add(trackedIssue); relativePathsInvolved.add(relativePath); return trackedIssue; } private TrackedIssue matchWithKnownFinding(Path relativePath, IssueMatcher issueMatcher, RawIssue newFinding) { var localMatchingResult = issueMatcher.matchWith(new RawIssueFindingMatchingAttributeMapper(), List.of(newFinding)); return localMatchingResult.getMatchOpt(newFinding) .map(knownFinding -> updateKnownFindingWithRawIssueData(knownFinding, newFinding)) .orElseGet(() -> newlyKnownIssue(relativePath, newFinding)); } public static TrackedIssue updateKnownFindingWithRawIssueData(KnownFinding knownIssue, RawIssue rawIssue) { return new TrackedIssue(knownIssue.getId(), rawIssue.getMessage(), knownIssue.getIntroductionDate(), false, rawIssue.getSeverity(), rawIssue.getRuleType(), rawIssue.getRuleKey(), TextRangeUtils.getTextRangeWithHash(rawIssue.getTextRange(), rawIssue.getClientInputFile()), TextRangeUtils.getLineWithHash(rawIssue.getTextRange(), rawIssue.getClientInputFile()), knownIssue.getServerKey(), rawIssue.getImpacts(), rawIssue.getFlows(), rawIssue.getQuickFixes(), rawIssue.getVulnerabilityProbability(), null, null, rawIssue.getRuleDescriptionContextKey(), rawIssue.getCleanCodeAttribute(), rawIssue.getFileUri()); } private TrackedIssue newlyKnownIssue(Path relativePath, RawIssue rawFinding) { newIssuesFound++; var introductionDate = introductionDateProvider.determineIntroductionDate(relativePath, rawFinding.getLineNumbers()); return IssueMapper.toTrackedIssue(rawFinding, introductionDate); } public Map> getIssuesPerFile() { return issuesPerFile; } public Map> getSecurityHotspotsPerFile() { return securityHotspotsPerFile; } public Set getRelativePathsInvolved() { return relativePathsInvolved; } public long countNewIssues() { return newIssuesFound; } public long countRemainingUnmatchedIssues() { return issueMatchersByFile.values().stream().mapToLong(IssueMatcher::getUnmatchedIssuesCount).sum(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/RawIssueFindingMatchingAttributeMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.analysis.RawIssue; public class RawIssueFindingMatchingAttributeMapper implements MatchingAttributesMapper { @Override public String getRuleKey(RawIssue issue) { return issue.getRuleKey(); } @Override public Optional getLine(RawIssue issue) { return issue.getLine(); } @Override public Optional getTextRangeHash(RawIssue issue) { return issue.getTextRangeHash(); } @Override public Optional getLineHash(RawIssue issue) { return issue.getLineHash(); } @Override public String getMessage(RawIssue issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(RawIssue issue) { return Optional.empty(); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/ServerHotspotMatchingAttributesMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; public class ServerHotspotMatchingAttributesMapper implements MatchingAttributesMapper { @Override public String getRuleKey(ServerHotspot issue) { return issue.getRuleKey(); } @Override public Optional getLine(ServerHotspot issue) { return Optional.of(issue.getTextRange().getStartLine()); } @Override public Optional getTextRangeHash(ServerHotspot issue) { var textRange = issue.getTextRange(); if (textRange instanceof TextRangeWithHash textRangeWithHash) { return Optional.of(textRangeWithHash.getHash()); } return Optional.empty(); } @Override public Optional getLineHash(ServerHotspot issue) { // no line hash for hotspots return Optional.empty(); } @Override public String getMessage(ServerHotspot issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(ServerHotspot issue) { return Optional.of(issue.getKey()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/ServerIssueMatchingAttributesMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; public class ServerIssueMatchingAttributesMapper implements MatchingAttributesMapper> { @Override public String getRuleKey(ServerIssue issue) { return issue.getRuleKey(); } @Override public Optional getLine(ServerIssue issue) { if (issue instanceof LineLevelServerIssue lineLevelServerIssue) { return Optional.of(lineLevelServerIssue.getLine()); } if (issue instanceof RangeLevelServerIssue rangeLevelServerIssue) { return Optional.of(rangeLevelServerIssue.getTextRange().getStartLine()); } return Optional.empty(); } @Override public Optional getTextRangeHash(ServerIssue issue) { if (issue instanceof RangeLevelServerIssue rangeLevelServerIssue) { return Optional.of(rangeLevelServerIssue.getTextRange().getHash()); } return Optional.empty(); } @Override public Optional getLineHash(ServerIssue issue) { if (issue instanceof LineLevelServerIssue lineLevelServerIssue) { return Optional.of(lineLevelServerIssue.getLineHash()); } return Optional.empty(); } @Override public String getMessage(ServerIssue issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(ServerIssue issue) { return Optional.of(issue.getKey()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/TrackedIssueFindingMatchingAttributeMapper.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.matching; import java.util.Optional; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; public class TrackedIssueFindingMatchingAttributeMapper implements MatchingAttributesMapper { @Override public String getRuleKey(TrackedIssue issue) { return issue.getRuleKey(); } @Override public Optional getLine(TrackedIssue issue) { var textRange = issue.getTextRangeWithHash(); if (textRange == null) { return Optional.empty(); } return Optional.of(textRange.getStartLine()); } @Override public Optional getTextRangeHash(TrackedIssue issue) { var textRange = issue.getTextRangeWithHash(); if (textRange == null) { return Optional.empty(); } return Optional.of(textRange.getHash()); } @Override public Optional getLineHash(TrackedIssue issue) { var lineWithHash = issue.getLineWithHash(); if (lineWithHash != null) { return Optional.of(lineWithHash.getHash()); } return Optional.empty(); } @Override public String getMessage(TrackedIssue issue) { return issue.getMessage(); } @Override public Optional getServerIssueKey(TrackedIssue issue) { return Optional.ofNullable(issue.getServerKey()); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/matching/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.tracking.matching; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.tracking; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/streaming/Alarm.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking.streaming; import java.time.Duration; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; public class Alarm { private final Duration duration; private final Runnable endRunnable; private final ScheduledExecutorService executorService; private ScheduledFuture scheduledFuture; public Alarm(String name, Duration duration, Runnable endRunnable) { this.duration = duration; this.endRunnable = endRunnable; this.executorService = FailSafeExecutors.newSingleThreadScheduledExecutor(name); } public void schedule() { // if already scheduled, don't re-schedule if (scheduledFuture == null) { scheduledFuture = executorService.schedule(this::notifyEnd, duration.toMillis(), TimeUnit.MILLISECONDS); } } public void reset() { cancelRunning(); schedule(); } private void notifyEnd() { if (!executorService.isShutdown()) { scheduledFuture = null; endRunnable.run(); } } public void shutdownNow() { cancelRunning(); executorService.shutdownNow(); } private void cancelRunning() { if (scheduledFuture != null) { scheduledFuture.cancel(false); } scheduledFuture = null; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/streaming/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.tracking.streaming; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/History.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class History { private final Map receivedMessages = new ConcurrentHashMap<>(); public void recordMessage(String message) { receivedMessages.put(message, Instant.now()); } public boolean exists(String message) { return receivedMessages.containsKey(message); } public void forgetOlderThan(Duration expiryDuration) { var now = Instant.now(); receivedMessages.values().removeIf(messageDate -> messageDate.isBefore(now.minus(expiryDuration))); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/SonarCloudWebSocket.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.IOException; import java.net.URI; import java.net.http.WebSocket; import java.nio.channels.UnresolvedAddressException; import java.time.Duration; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.http.WebSocketClient; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; import org.sonarsource.sonarlint.core.serverapi.push.parsing.EventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.IssueChangedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotChangedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotClosedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotRaisedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.TaintVulnerabilityClosedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.TaintVulnerabilityRaisedEventParser; import org.sonarsource.sonarlint.core.websocket.parsing.SmartNotificationEventParser; public class SonarCloudWebSocket { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Map> parsersByTypeForProjectFilter = Map.of( "QualityGateChanged", new SmartNotificationEventParser("QUALITY_GATE"), "IssueChanged", new IssueChangedEventParser(), "SecurityHotspotClosed", new SecurityHotspotClosedEventParser(), "SecurityHotspotRaised", new SecurityHotspotRaisedEventParser(), "SecurityHotspotChanged", new SecurityHotspotChangedEventParser(), "TaintVulnerabilityClosed", new TaintVulnerabilityClosedEventParser(), "TaintVulnerabilityRaised", new TaintVulnerabilityRaisedEventParser()); private static final Map> parsersByTypeForProjectUserFilter = Map.of( "MyNewIssues", new SmartNotificationEventParser("NEW_ISSUES")); private static final Map> parsersByType = Stream.of(parsersByTypeForProjectFilter, parsersByTypeForProjectUserFilter) .flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); private static final String PROJECT_FILTER_TYPE = "PROJECT"; private static final String PROJECT_USER_FILTER_TYPE = "PROJECT_USER"; private static final Gson gson = new Gson(); private CompletableFuture wsFuture; private final History history = new History(); private final ScheduledExecutorService sonarCloudWebSocketScheduler = FailSafeExecutors.newSingleThreadScheduledExecutor("sonarcloud-websocket-scheduled-jobs"); private final AtomicBoolean closingInitiated = new AtomicBoolean(false); private final CompletableFuture webSocketInputClosed = new CompletableFuture<>(); public static SonarCloudWebSocket create(URI webSocketsEndpointUri, WebSocketClient webSocketClient, Consumer serverEventConsumer, Runnable connectionEndedRunnable) { var webSocket = new SonarCloudWebSocket(); var currentThreadOutput = SonarLintLogger.get().getTargetForCopy(); LOG.info("Creating WebSocket connection to " + webSocketsEndpointUri); webSocket.wsFuture = webSocketClient.createWebSocketConnection(webSocketsEndpointUri, rawEvent -> webSocket.handleRawMessage(rawEvent, serverEventConsumer), () -> { webSocket.webSocketInputClosed.complete(null); // Don't call the callback if the client has triggered the closing if (!webSocket.closingInitiated.get()) { connectionEndedRunnable.run(); } }); webSocket.wsFuture.thenAccept(ws -> { SonarLintLogger.get().setTarget(currentThreadOutput); webSocket.sonarCloudWebSocketScheduler.scheduleAtFixedRate(webSocket::cleanUpMessageHistory, 0, 5, TimeUnit.MINUTES); webSocket.sonarCloudWebSocketScheduler.schedule(connectionEndedRunnable, 119, TimeUnit.MINUTES); webSocket.sonarCloudWebSocketScheduler.scheduleAtFixedRate(() -> keepAlive(ws), 9, 9, TimeUnit.MINUTES); }); webSocket.wsFuture.exceptionally(t -> { SonarLintLogger.get().setTarget(currentThreadOutput); LOG.error("Error while trying to create WebSocket connection for " + webSocketsEndpointUri, t); return null; }); return webSocket; } private static void keepAlive(WebSocket ws) { ws.sendText("{\"action\": \"keep_alive\",\"statusCode\":200}", true); } private void cleanUpMessageHistory() { history.forgetOlderThan(Duration.ofMinutes(1)); } private SonarCloudWebSocket() { } public void subscribe(String projectKey) { send("subscribe", projectKey, parsersByTypeForProjectFilter, PROJECT_FILTER_TYPE); send("subscribe", projectKey, parsersByTypeForProjectUserFilter, PROJECT_USER_FILTER_TYPE); } public void unsubscribe(String projectKey) { send("unsubscribe", projectKey, parsersByTypeForProjectFilter, PROJECT_FILTER_TYPE); send("unsubscribe", projectKey, parsersByTypeForProjectUserFilter, PROJECT_USER_FILTER_TYPE); } private void send(String messageType, String projectKey, Map> parsersByType, String filter) { var eventsKey = parsersByType.keySet().toArray(new String[0]); Arrays.sort(eventsKey); var payload = new WebSocketEventSubscribePayload(messageType, eventsKey, filter, projectKey); var jsonString = gson.toJson(payload); try { // wait for the message to be sent to not fail the next one, see SLCORE-787 this.wsFuture.thenCompose(ws -> { LOG.debug("sent '" + messageType + "' for project '" + projectKey + "'"); return ws.sendText(jsonString, true); }).join(); } catch (Exception e) { LOG.error("Error when sending a message in the WebSocket channel", e); } } private void handleRawMessage(String message, Consumer serverEventConsumer) { if (history.exists(message)) { // SC implements at least 1 time delivery, so we need to de-duplicate the messages return; } history.recordMessage(message); try { var wsEvent = gson.fromJson(message, WebSocketEvent.class); parse(wsEvent).ifPresent(serverEventConsumer); LOG.debug("Server event received: " + message, LogOutput.Level.DEBUG); } catch (Exception e) { LOG.error("Malformed event received: " + message, e); } } private static Optional parse(WebSocketEvent event) { var eventType = event.event; if (eventType == null) { return Optional.empty(); } if (parsersByType.containsKey(eventType)) { return tryParsing(parsersByType.get(eventType), event); } else { LOG.error("Unknown '{}' event type ", eventType); return Optional.empty(); } } private static Optional tryParsing(EventParser eventParser, WebSocketEvent event) { try { return eventParser.parse(event.data.toString()); } catch (Exception e) { LOG.error("Cannot parse '{}' received event", event.event, e); return Optional.empty(); } } public void close(String reason) { LOG.debug("Closing SonarCloud WebSocket connection, reason={}...", reason); this.closingInitiated.set(true); if (this.wsFuture != null) { // output could already be closed if an error occurred try { // Check if the future completed exceptionally before trying to get the result if (this.wsFuture.isCompletedExceptionally()) { LOG.debug("WebSocket connection was already closed, skipping close operation"); } else if (this.wsFuture.isDone()) { this.wsFuture.thenAccept(ws -> close(ws, this.webSocketInputClosed)).get(); } else { // Future is still pending, cancel it this.wsFuture.cancel(true); LOG.debug("WebSocket connection was still pending, cancelled"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { LOG.error("Cannot close the WebSocket output", e); } this.wsFuture = null; } if (!MoreExecutors.shutdownAndAwaitTermination(sonarCloudWebSocketScheduler, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop SonarCloud WebSocket job scheduler in a timely manner"); } } private static void close(WebSocket ws, CompletableFuture webSocketInputClosed) { if (!ws.isOutputClosed()) { try { // close output ws.sendClose(WebSocket.NORMAL_CLOSURE, "").get(); LOG.debug("Waiting for SonarCloud WebSocket input to be closed..."); webSocketInputClosed.get(10, TimeUnit.SECONDS); LOG.debug("SonarCloud WebSocket closed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { handleExecutionException(e); } catch (TimeoutException e) { handleTimeoutException(ws, e); } } } private static void handleExecutionException(ExecutionException e) { // This might fail with an "IOException: Output closed" in case the WebSocket was closed by the server or reached EOL which // is fine and should not throw an error message to the user misleading them that something is not working correctly. var cause = e.getCause(); if (cause instanceof UnresolvedAddressException || (cause instanceof IOException && (cause.getMessage().contains("Output closed") || cause.getMessage().contains("closed output")))) { LOG.debug("WebSocket could not be closed gracefully", e); } else { LOG.error("Cannot close the WebSocket output", e); } } private static void handleTimeoutException(WebSocket ws, TimeoutException e) { LOG.error("The WebSocket input did not close in a timely manner", e); if (!ws.isInputClosed()) { // close input ws.abort(); } } public boolean isOpen() { if (wsFuture == null || !wsFuture.isDone() || wsFuture.isCompletedExceptionally() || wsFuture.isCancelled()) { return false; } var ws = wsFuture.getNow(null); return ws != null && !ws.isInputClosed() && !ws.isOutputClosed(); } private static class WebSocketEvent { private String event; private JsonObject data; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketEventSubscribePayload.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; public class WebSocketEventSubscribePayload { private final String action; private final String[] events; private final String filterType; private final String project; public WebSocketEventSubscribePayload(String action, String[] events, String filterType, String project) { this.action = action; this.events = events; this.filterType = filterType; this.project = project; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketManager.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; import java.net.URI; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.SonarServerEventReceivedEvent; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; import org.springframework.context.ApplicationEventPublisher; public class WebSocketManager { private static final SonarLintLogger LOG = SonarLintLogger.get(); private SonarCloudWebSocket sonarCloudWebSocket; private final Set connectionIdsInterestedInNotifications = new HashSet<>(); private String connectionIdUsedToCreateConnection; private final Map subscribedProjectKeysByConfigScopes = new HashMap<>(); private final ExecutorService executorService = FailSafeExecutors.newSingleThreadExecutor("sonarlint-websocket-subscriber"); private final ApplicationEventPublisher eventPublisher; private final SonarQubeClientManager sonarQubeClientManager; private final ConfigurationRepository configurationRepository; private final URI websocketEndpointUri; public WebSocketManager(ApplicationEventPublisher eventPublisher, SonarQubeClientManager sonarQubeClientManager, ConfigurationRepository configurationRepository, URI websocketEndpointUri) { this.eventPublisher = eventPublisher; this.sonarQubeClientManager = sonarQubeClientManager; this.configurationRepository = configurationRepository; this.websocketEndpointUri = websocketEndpointUri; } private void handleSonarServerEvent(SonarServerEvent event) { connectionIdsInterestedInNotifications.forEach(id -> eventPublisher.publishEvent(new SonarServerEventReceivedEvent(id, event))); } public void forgetConnection(String connectionId, String reason) { var previouslyInterestedInNotifications = connectionIdsInterestedInNotifications.remove(connectionId); if (!previouslyInterestedInNotifications) { return; } if (connectionIdsInterestedInNotifications.isEmpty()) { closeSocket(reason); subscribedProjectKeysByConfigScopes.clear(); } else if (this.connectionIdUsedToCreateConnection.equals(connectionId)) { // stop using the credentials, switch to another connection var otherConnectionId = connectionIdsInterestedInNotifications.stream().findAny().orElseThrow(); removeProjectsFromSubscriptionListForConnection(connectionId); this.reopenConnection(otherConnectionId, reason + ", reopening for other SC connection"); } else { configurationRepository.getBoundScopesToConnection(connectionId) .forEach(configScope -> forget(configScope.getConfigScopeId())); } } private void removeProjectsFromSubscriptionListForConnection(String updatedConnectionId) { var configurationScopesToUnsubscribe = configurationRepository.getBoundScopesToConnection(updatedConnectionId); for (var configScope : configurationScopesToUnsubscribe) { subscribedProjectKeysByConfigScopes.remove(configScope.getConfigScopeId()); } } /** * @return the connection if it was or has been opened, else empty */ public Optional createConnectionIfNeeded(String connectionId) { connectionIdsInterestedInNotifications.add(connectionId); if (hasOpenConnection()) { return Optional.of(sonarCloudWebSocket); } try { return sonarQubeClientManager.getValidWebSocketClient(connectionId) .map(webSocketClient -> { this.sonarCloudWebSocket = SonarCloudWebSocket.create(this.websocketEndpointUri, webSocketClient, this::handleSonarServerEvent, this::reopenConnectionOnClose); this.connectionIdUsedToCreateConnection = connectionId; return sonarCloudWebSocket; }); } catch (Exception e) { LOG.error("Error while creating WebSocket connection", e); return Optional.empty(); } } public void reopenConnection(String connectionId, String reason) { closeSocket(reason); createConnectionIfNeeded(connectionId) .ifPresent(connection -> resubscribeAll()); } protected void reopenConnectionOnClose() { executorService.execute(() -> { var connectionId = connectionIdsInterestedInNotifications.stream().findFirst().orElse(null); if (this.sonarCloudWebSocket != null && connectionId != null) { // If connection already exists, close it and create new one before it expires on its own this.reopenConnection(connectionId, "WebSocket was closed by server or reached EOL"); } }); } public void closeSocketIfNoMoreNeeded() { if (subscribedProjectKeysByConfigScopes.isEmpty()) { closeSocket("No more bound project"); } } public void subscribe(String configScopeId, Binding binding) { createConnectionIfNeeded(binding.connectionId()) .ifPresent(connection -> { var projectKey = binding.sonarProjectKey(); if (subscribedProjectKeysByConfigScopes.containsKey(configScopeId) && !subscribedProjectKeysByConfigScopes.get(configScopeId).equals(projectKey)) { this.forget(configScopeId); } if (!subscribedProjectKeysByConfigScopes.containsValue(projectKey)) { connection.subscribe(projectKey); } subscribedProjectKeysByConfigScopes.put(configScopeId, projectKey); }); } private void resubscribeAll() { var uniqueProjectKeys = new HashSet<>(subscribedProjectKeysByConfigScopes.values()); uniqueProjectKeys.forEach(projectKey -> sonarCloudWebSocket.subscribe(projectKey)); } public void closeSocket(String reason) { if (this.sonarCloudWebSocket != null) { var socket = this.sonarCloudWebSocket; this.sonarCloudWebSocket = null; this.connectionIdUsedToCreateConnection = null; socket.close(reason); } } public boolean hasOpenConnection() { return sonarCloudWebSocket != null && sonarCloudWebSocket.isOpen(); } public void forget(String configScopeId) { var projectKey = subscribedProjectKeysByConfigScopes.remove(configScopeId); if (projectKey != null && !subscribedProjectKeysByConfigScopes.containsValue(projectKey) && hasOpenConnection()) { sonarCloudWebSocket.unsubscribe(projectKey); } } public Map getSubscribedProjectKeysByConfigScopes() { return subscribedProjectKeysByConfigScopes; } public boolean isInterestedInNotifications(String connectionId) { return connectionIdsInterestedInNotifications.contains(connectionId); } public Set getConnectionIdsInterestedInNotifications() { return connectionIdsInterestedInNotifications; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/WebSocketService.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopeRemovedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.event.ConnectionCredentialsChangedEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.SERVER_SENT_EVENTS; public class WebSocketService { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final boolean shouldEnableWebSockets; private final ConnectionConfigurationRepository connectionConfigurationRepository; private final ConfigurationRepository configurationRepository; private final Map webSocketsByRegion; private final ExecutorService executorService = FailSafeExecutors.newSingleThreadExecutor("sonarlint-websocket-subscriber"); public WebSocketService(ConnectionConfigurationRepository connectionConfigurationRepository, ConfigurationRepository configurationRepository, SonarQubeClientManager sonarQubeClientManager, InitializeParams params, SonarCloudActiveEnvironment sonarCloudActiveEnvironment, ApplicationEventPublisher eventPublisher) { this.connectionConfigurationRepository = connectionConfigurationRepository; this.configurationRepository = configurationRepository; this.shouldEnableWebSockets = params.getBackendCapabilities().contains(SERVER_SENT_EVENTS); this.webSocketsByRegion = Map.of( SonarCloudRegion.US, new WebSocketManager(eventPublisher, sonarQubeClientManager, configurationRepository, sonarCloudActiveEnvironment.getWebSocketsEndpointUri(SonarCloudRegion.US)), SonarCloudRegion.EU, new WebSocketManager(eventPublisher, sonarQubeClientManager, configurationRepository, sonarCloudActiveEnvironment.getWebSocketsEndpointUri(SonarCloudRegion.EU))); } @EventListener public void handleEvent(BindingConfigChangedEvent bindingConfigChangedEvent) { if (!shouldEnableWebSockets) { return; } executorService.execute(() -> { considerScope(bindingConfigChangedEvent.configScopeId()); // possible change of region for the binding; need to unsubscribe from the old region (subscription to the new one will be done in considerScope) if (didChangeRegion(bindingConfigChangedEvent.previousConfig(), bindingConfigChangedEvent.newConfig())) { // will only enter this block if previous connection (and connectionId) existed var previousRegion = ((SonarCloudConnectionConfiguration) connectionConfigurationRepository .getConnectionById(bindingConfigChangedEvent.previousConfig().connectionId())).getRegion(); webSocketsByRegion.get(previousRegion).forget(bindingConfigChangedEvent.configScopeId()); webSocketsByRegion.get(previousRegion).closeSocketIfNoMoreNeeded(); } }); } @EventListener public void handleEvent(ConfigurationScopesAddedWithBindingEvent configurationScopesAddedEvent) { if (!shouldEnableWebSockets) { return; } executorService.execute(() -> considerAllBoundConfigurationScopes(configurationScopesAddedEvent.getConfigScopeIds())); } @EventListener public void handleEvent(ConfigurationScopeRemovedEvent configurationScopeRemovedEvent) { if (!shouldEnableWebSockets) { return; } var removedConfigurationScopeId = configurationScopeRemovedEvent.getRemovedConfigurationScopeId(); executorService.execute(() -> webSocketsByRegion.forEach((region, webSocketManager) -> { webSocketManager.forget(removedConfigurationScopeId); webSocketManager.closeSocketIfNoMoreNeeded(); }) ); } @EventListener public void handleEvent(ConnectionConfigurationAddedEvent connectionConfigurationAddedEvent) { if (!shouldEnableWebSockets) { return; } // This is only to handle the case where binding was invalid (connection did not exist) and became valid (matching connection was created) executorService.execute(() -> considerConnection(connectionConfigurationAddedEvent.addedConnectionId())); } @EventListener public void handleEvent(ConnectionConfigurationUpdatedEvent connectionConfigurationUpdatedEvent) { if (!shouldEnableWebSockets) { return; } var updatedConnectionId = connectionConfigurationUpdatedEvent.updatedConnectionId(); executorService.execute(() -> { if (didDisableNotifications(updatedConnectionId)) { webSocketsByRegion.forEach((region, webSocketManager) -> webSocketManager.forgetConnection(updatedConnectionId, "Notifications were disabled") ); } else if (didEnableNotifications(updatedConnectionId)) { considerConnection(updatedConnectionId); } }); } @EventListener public void handleEvent(ConnectionConfigurationRemovedEvent connectionConfigurationRemovedEvent) { if (!shouldEnableWebSockets) { return; } String removedConnectionId = connectionConfigurationRemovedEvent.removedConnectionId(); executorService.execute(() -> webSocketsByRegion.forEach((region, webSocketManager) -> webSocketManager.forgetConnection(removedConnectionId, "Connection was removed") ) ); } @EventListener public void handleEvent(ConnectionCredentialsChangedEvent connectionCredentialsChangedEvent) { if (!shouldEnableWebSockets) { return; } var connectionId = connectionCredentialsChangedEvent.getConnectionId(); executorService.execute(() -> { if (isEligibleConnection(connectionId) && isInterestedInNotifications(connectionId)) { var region = ((SonarCloudConnectionConfiguration) connectionConfigurationRepository.getConnectionById(connectionId)).getRegion(); webSocketsByRegion.get(region).reopenConnection(connectionId, "Credentials have changed"); } }); } private void considerConnection(String connectionId) { var configScopeIds = configurationRepository.getBoundScopesToConnection(connectionId) .stream().map(BoundScope::getConfigScopeId) .collect(Collectors.toSet()); considerAllBoundConfigurationScopes(configScopeIds); } private void considerAllBoundConfigurationScopes(Set configScopeIds) { for (String scopeId : configScopeIds) { considerScope(scopeId); } } private void considerScope(String scopeId) { var binding = getCurrentBinding(scopeId); if (binding != null && isEligibleConnection(binding.connectionId())) { var connection = requireNonNull(connectionConfigurationRepository.getConnectionById(binding.connectionId())); var region = ((SonarCloudConnectionConfiguration) connection).getRegion(); webSocketsByRegion.get(region).subscribe(scopeId, binding); } else if (isSubscribedToAProject(scopeId)) { // no binding or binding is not eligible, unsubscribe from all regions if it was subscribed to a project webSocketsByRegion.forEach((region, webSocketManager) -> { webSocketManager.forget(scopeId); webSocketManager.closeSocketIfNoMoreNeeded(); }); } } private boolean isInterestedInNotifications(String connectionId) { return webSocketsByRegion.values().stream().anyMatch(webSocketManager -> webSocketManager.isInterestedInNotifications(connectionId)); } private boolean isEligibleConnection(String connectionId) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); return connection != null && connection.getKind().equals(ConnectionKind.SONARCLOUD) && !connection.isDisableNotifications(); } private boolean didChangeRegion(BindingConfiguration previousBindingConfiguration, BindingConfiguration newBindingConfiguration) { var previousConnectionId = previousBindingConfiguration.connectionId(); var previousConnection = previousConnectionId != null ? connectionConfigurationRepository.getConnectionById(previousConnectionId) : null; var newConnectionId = newBindingConfiguration.connectionId(); var newConnection = newConnectionId != null ? connectionConfigurationRepository.getConnectionById(newConnectionId) : null; if (newConnection == null || previousConnection == null) { // nothing to do return false; } else if (previousConnection instanceof SonarCloudConnectionConfiguration previousConn && newConnection instanceof SonarCloudConnectionConfiguration newConn) { // was SonarCloud connection and still is - check if region changed return previousConn.getRegion() != newConn.getRegion(); } return false; } @CheckForNull private Binding getCurrentBinding(String configScopeId) { var bindingConfiguration = configurationRepository.getBindingConfiguration(configScopeId); if (bindingConfiguration != null && bindingConfiguration.isBound()) { return new Binding(requireNonNull(bindingConfiguration.connectionId()), requireNonNull(bindingConfiguration.sonarProjectKey())); } return null; } private boolean didDisableNotifications(String connectionId) { if (isInterestedInNotifications(connectionId)) { var connection = connectionConfigurationRepository.getConnectionById(connectionId); return connection != null && connection.getKind().equals(ConnectionKind.SONARCLOUD) && connection.isDisableNotifications(); } return false; } private boolean didEnableNotifications(String connectionId) { return isEligibleConnection(connectionId) && !isInterestedInNotifications(connectionId); } private boolean isSubscribedToAProject(String configScopeId) { for (var webSocketManager : webSocketsByRegion.values()) { var subscribedProjectKey = webSocketManager.getSubscribedProjectKeysByConfigScopes().get(configScopeId); if (subscribedProjectKey != null) { // we are interested if it was subscribed to a project in any region return true; } } return false; } public boolean hasOpenConnection(SonarCloudRegion region) { return webSocketsByRegion.get(region).hasOpenConnection(); } @PreDestroy public void shutdown() { if (!MoreExecutors.shutdownAndAwaitTermination(executorService, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop websockets subscriber service in a timely manner"); } webSocketsByRegion.forEach((region, webSocketManager) -> { webSocketManager.closeSocket("Backend is shutting down"); webSocketManager.getSubscribedProjectKeysByConfigScopes().clear(); webSocketManager.getConnectionIdsInterestedInNotifications().clear(); }); } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/events/SmartNotificationEvent.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket.events; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; public record SmartNotificationEvent(String message, String link, String project, String date, String category) implements SonarServerEvent { public SmartNotificationEvent(String message, String link, String project, String date, String category) { this.message = escapeHtml4(message); this.link = link; this.project = project; this.date = date; this.category = category; } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/events/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.websocket.events; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.websocket; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/parsing/SmartNotificationEventParser.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket.parsing; import com.google.gson.Gson; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.parsing.EventParser; import org.sonarsource.sonarlint.core.websocket.events.SmartNotificationEvent; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class SmartNotificationEventParser implements EventParser { private final Gson gson = new Gson(); private final String category; public SmartNotificationEventParser(String category) { this.category = category; } @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, SmartNotificationEventPayload.class); if (payload.isInvalid()) { SonarLintLogger.get().error("Invalid payload for 'SmartNotification' event of category '" + category + "': {}", jsonData); return Optional.empty(); } return Optional.of(new SmartNotificationEvent( payload.message, payload.link, payload.project, payload.date, category)); } private static class SmartNotificationEventPayload { private String message; private String link; private String project; private String date; private boolean isInvalid() { return isBlank(message) || isBlank(link) || isBlank(project) || date == null; } } } ================================================ FILE: backend/core/src/main/java/org/sonarsource/sonarlint/core/websocket/parsing/package-info.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.websocket.parsing; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/core/src/main/resources/ai/hooks/sonarqube_analysis_hook.js ================================================ #!/usr/bin/env node // SonarQube for IDE {{AGENT}} Hook - sonarqube_analysis_hook // Auto-generated script for Node.js // Connects AI Agents to SonarQube for IDE backend const http = require('node:http'); const STARTING_PORT = 64120; const ENDING_PORT = 64130; const EXPECTED_IDE_NAME = '{{AGENT}}'; const PORT_SCAN_TIMEOUT = 100; async function findBackendPort() { const portPromises = []; for (let port = STARTING_PORT; port <= ENDING_PORT; port++) { portPromises.push(checkPort(port)); } const results = await Promise.allSettled(portPromises); for (const result of results) { if (result.status === 'fulfilled' && result.value !== null) { return result.value; } } return null; } function checkPort(port) { return new Promise((resolve) => { const req = http.get({ hostname: 'localhost', port, path: '/sonarlint/api/status', timeout: PORT_SCAN_TIMEOUT, headers: { 'Origin': 'ai-agent://{{AGENT}}' } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const status = JSON.parse(data); if (status.ideName === EXPECTED_IDE_NAME) { resolve(port); } else { resolve(null); } } catch (e) { resolve(null); } }); }); req.on('error', () => { resolve(null); }); req.on('timeout', () => { req.destroy(); resolve(null); }); }); } function analyzeFile(port, filePath) { console.log(`Analyzing: ${filePath} (port ${port})`); const requestBody = JSON.stringify({ fileAbsolutePaths: [filePath] }); const options = { hostname: 'localhost', port, path: '/sonarlint/api/analysis/files', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(requestBody), 'Origin': 'ai-agent://{{AGENT}}' }, agent: new http.Agent({ keepAlive: false }), timeout: 1000 }; const req = http.request(options); req.on('socket', (socket) => { socket.unref(); }); req.on('error', (err) => { console.log(`Error: ${err.message}`); }); req.write(requestBody); req.end(); setImmediate(() => { process.exit(0); }); } // Read the event JSON from stdin let eventJson = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { eventJson += chunk; }); process.stdin.on('end', async () => { try { const event = JSON.parse(eventJson); const filePath = event.tool_info?.file_path; if (!filePath) { console.log('No file path in event'); return; } const port = await findBackendPort(); if (!port) { console.log('Backend not found'); process.exit(1); } analyzeFile(port, filePath); } catch (e) { console.log(`Error: ${e.message}`); process.exit(1); } }); ================================================ FILE: backend/core/src/main/resources/ai/hooks/sonarqube_analysis_hook.py ================================================ #!/usr/bin/env python3 # SonarQube for IDE {{AGENT}} Hook - sonarqube_analysis_hook # Auto-generated script for Python # Connects AI Agents to SonarQube for IDE backend import sys import json import urllib.request import urllib.error STARTING_PORT = 64120 ENDING_PORT = 64130 EXPECTED_IDE_NAME = '{{AGENT}}' PORT_SCAN_TIMEOUT = 0.1 def find_backend_port(): """Fast port discovery: find the correct SonarQube for IDE backend""" for port in range(STARTING_PORT, ENDING_PORT + 1): if check_port(port): return port return None def check_port(port): """Check if a port has a valid SonarQube for IDE backend""" try: url = f'http://localhost:{port}/sonarlint/api/status' req = urllib.request.Request(url, headers={'Origin': 'ai-agent://{{AGENT}}'}) with urllib.request.urlopen(req, timeout=PORT_SCAN_TIMEOUT) as response: if response.status == 200: data = json.loads(response.read().decode('utf-8')) ide_name = data.get('ideName') if ide_name == EXPECTED_IDE_NAME: return True except Exception: pass return False def analyze_file(port, file_path): """Call the analysis endpoint (fire-and-forget, non-blocking)""" print(f'Analyzing: {file_path} (port {port})') request_body = json.dumps({'fileAbsolutePaths': [file_path]}) url = f'http://localhost:{port}/sonarlint/api/analysis/files' req = urllib.request.Request( url, data=request_body.encode('utf-8'), headers={ 'Content-Type': 'application/json', 'Origin': 'ai-agent://{{AGENT}}' } ) try: response = urllib.request.urlopen(req, timeout=1) response.close() except Exception as e: print(f'Error: {e}') sys.exit(0) def main(): try: event_json = sys.stdin.read() event = json.loads(event_json) tool_info = event.get('tool_info', {}) file_path = tool_info.get('file_path') if not file_path: print('No file path in event') return port = find_backend_port() if not port: print('Backend not found') sys.exit(1) analyze_file(port, file_path) except json.JSONDecodeError as e: print(f'JSON error: {e}') sys.exit(1) except Exception as e: print(f'Error: {e}') sys.exit(1) if __name__ == '__main__': main() ================================================ FILE: backend/core/src/main/resources/ai/hooks/sonarqube_analysis_hook.sh ================================================ #!/bin/bash # SonarQube for IDE {{AGENT}} Hook - sonarqube_analysis_hook # Auto-generated script for Bash # Connects AI Agents to SonarQube for IDE backend set -e STARTING_PORT=64120 ENDING_PORT=64130 EXPECTED_IDE_NAME="{{AGENT}}" PORT_SCAN_TIMEOUT=0.1 find_backend_port() { for port in $(seq $STARTING_PORT $ENDING_PORT); do if check_port "$port"; then echo "$port" return 0 fi done return 1 } check_port() { local port=$1 local status_json=$(curl -s --max-time $PORT_SCAN_TIMEOUT -H "Origin: ai-agent://{{AGENT}}" "http://localhost:${port}/sonarlint/api/status" 2>/dev/null) if [ $? -ne 0 ]; then return 1 fi local ide_name=$(echo "$status_json" | jq -r '.ideName // empty' 2>/dev/null) if [ "$ide_name" = "$EXPECTED_IDE_NAME" ]; then return 0 fi return 1 } analyze_file() { local port=$1 local file_path=$2 echo "Analyzing: $file_path (port $port)" local request_body=$(jq -n --arg file "$file_path" '{fileAbsolutePaths: [$file]}') curl -s --max-time 5 -X POST \ -H "Content-Type: application/json" \ -H "Origin: ai-agent://{{AGENT}}" \ -H "Connection: close" \ -d "$request_body" \ "http://localhost:${port}/sonarlint/api/analysis/files" \ > /dev/null 2>&1 & return 0 } EVENT_JSON=$(cat) if ! command -v jq &> /dev/null; then echo "jq not found" exit 1 fi FILE_PATH=$(echo "$EVENT_JSON" | jq -r '.tool_info.file_path // empty' 2>/dev/null || echo "") if [ -z "$FILE_PATH" ]; then echo "No file path in event" exit 0 fi BACKEND_PORT=$(find_backend_port) if [ $? -ne 0 ]; then echo "Backend not found" exit 1 fi analyze_file "$BACKEND_PORT" "$FILE_PATH" ================================================ FILE: backend/core/src/main/resources/clean-code-principles/defense_in_depth.html ================================================

Defense-In-Depth

Applications and infrastructure benefit greatly from relying on multiple security mechanisms layered on top of each other. If one security mechanism fails, there is a high probability that the subsequent layers of security will successfully defend against the attack.

A non-exhaustive list of these code protection ramparts includes the following:

  • Minimizing the attack surface of the code
  • Application of the principle of least privilege
  • Validation and sanitization of data
  • Encrypting incoming, outgoing, or stored data with secure cryptography
  • Ensuring that internal errors cannot disrupt the overall runtime
  • Separation of tasks and access to information

Note that these layers must be simple enough to use in an everyday workflow. Security measures should not break usability.

================================================ FILE: backend/core/src/main/resources/clean-code-principles/never_trust_user_input.html ================================================

Never Trust User Input

Applications must treat all user input and, more generally, all third-party data as attacker-controlled data.

The application must determine where the third-party data comes from and treat that data source as an attack vector. Two rules apply:

First, before using it in the application's business logic, the application must validate the attacker-controlled data against predefined formats, such as:

  • Character sets
  • Sizes
  • Types
  • Or any strict schema

Second, the application must sanitize string data before inserting it into interpreted contexts (client-side code, file paths, SQL queries). Unsanitized code can corrupt the application's logic.

================================================ FILE: backend/core/src/main/resources/context-rule-description/others_section_html_content.html ================================================

How can I fix it in another component or framework?

Although the main framework or component you use in your project is not listed, you may find helpful content in the instructions we provide.

Caution: The libraries mentioned in these instructions may not be appropriate for your code.

  • Do use libraries that are compatible with the frameworks you are using.
  • Don't blindly copy and paste the fix-ups into your code.

Help us improve

Let us know if the instructions we provide do not work for you. Tell us which framework you use and why our solution does not work by submitting an idea on the SonarLint product-board.

Submit an idea

We will do our best to provide you with more relevant instructions in the future.

================================================ FILE: backend/core/src/main/resources/ondemand/plugins.properties ================================================ cfamily.version=${cfamily.version} cs.version=${csharp.version} omnisharp.version=${omnisharp.version} ================================================ FILE: backend/core/src/main/resources/ondemand/sonarsource-public.key ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- Version: Hockeypuck 2.2 Comment: Hostname: xsFNBGCGrYsBEAC/Ws37TXMujQ4z2ioXlh5SlrWaCzdN5RSBAQEKaiuuQeuwdWku bsnhI2f7YgxfJh2if6hCsGeWx3Wd2paLT9IqJbnIltOzHQkYXajIJrJVDep31wQD FsjQS8DWdRGkrldc2ClWZs1PAGC4Snp9bNYrnlE8Z1uHVnmN2R0aQ3v7PGw2qpQ9 XxsQl9m30hMDb4IZBOKy92PC+xNpb6dgee3HJ8uJ2t/nTUCuP1FsMPGP3crbK9po UOUigIWMKNnYTyHbx+p22EQIn3iKQU4DQTeZm1/rUnfuULp2Zhl+fTs6U/czCrdr 7DN4MCzthK7DMhDHH7/uVk53+e0oe0FJZSxYE1ppjvLz4Ox7xMHrlOMFIqb9JOgn exUDV34KcPByHqY4ff7IL94Tx7YAwEplnJYBEfb0sYfmjai4PCFj74gjjCmhQUm8 5Cbm23JvDGck9W75wc6qj7wcFpZrFtfpOsz10YsprM5TcmK9rEIV+o+bRqoNs5hS +heZmdz7LoWJgarJnlkPjDDOXW54bA5kS8ARlkxllzZ+f0BwaN/HBNbVv3gkBHUX YOxphjESdv/WByNQMgzoIBiUt02RqAJg9PECLJSjSfFzd2F9g7Lmc0TUdA/kLEZm DqgrDjPkfkwnSqCglI38Z/gcVoSDN2iYhEIfuGoZXbjG4IDVuFYyGZjimQARAQAB zShTb25hclNvdXJjZSBTLkEuIDxpbmZyYUBzb25hcnNvdXJjZS5jb20+wsGUBBMB CgA+FiEEZ58e6SsZYJ3oFv3oHbGY+TUl7BoFAmCGrYsCGwEFCQlmAYAFCwkIBwIG FQoJCAsCBBYCAwECHgECF4AACgkQHbGY+TUl7Bpn+w/8DZjbw5SqguIMnIN1lmZC DCNSKk7CJNpkO7ZjXYZo9ZzGlULse4wlqoW5cVH3NiOATV4BnQQotSoeBr8RFdh0 TI+Zbt2wKv3j4+LxIlalfnYrj77SRh43qqmAKxVS5HAdEXfHNfBtNV88CJTTByX/ PAw7vIbI+6YwwIP/ps33GrESjDZNefdLuTvq3FwrTNicoWnXrIFbs01lNfy6NTfk 5ZrVHjmTQxHrh0VY4vNZNQYnTzET3fMmhudlIxXPuuNSPl2X1UaTVFNHSwK/IsOr m8oWZfG++HgbVmR9YG1Ci7tYTBc+gbp8xel5FjzKcBLQfZwqsnz/Gn3PlPCwKXNI uq0Gp925P86scOlCz73Wfy8vde3rc6j+hzlgKuwgunJvl+cyWAyTdvTkcpCN6QJk R6ZuXrNkqCzbxT0NNoWEHSDJmJ8ECqJRfza6ag7lReWaT/dGZ/R9a19pbGmGXuqq qcwE9hRognxejhAn7mfVpLEsGJwrQEeVQCKQVFIZkFpUr3oYOIPppGxguM97ZNvY uZnHq9UwufRMR83h0XWWdTqurYoAcHkjeXH0DKXkM9kQg86FSf/KSWj9cI8/q3en VM+HboxrzY8Cc91IwXLOgV1ipowwy8fcnyU8GD+P3bvh1J/nVgzm+NTJ4RIfbDDq 4Q6vWIDIAfqnRK3aTr2atSTOwU0EaSxelAEQAMjFbyhLN7gPxyvuKGcBP5L7b4UK kDSPPqBXHmv1KiEspKSF2cy84F6xdcbfuk3/V0wieu7/1S4Ro7CawBAS8U0VcXEw X0MdbBaB0nlVV1QwAsF2e9bDbVpFZb9kOiBWRwXLANJu0NnH0CW3IB4ba23JS+72 0P6d3Jf3ylvF/I/7HvNGyjxRQ4B7wGX+IzK9rVf/UjQ9ce8oDVIv6gki1z28fCGm nv05fIvWTcU1+yUWP4cnDtfOGjHh/ISps6cfPM2xruhCVGQ5UQD5Jabswx5ZXCsa HLRRBinhlElfPFBlc5e123RWRjgfrOGQtnkom7agCHmUlbWL2QEFrw3Jab/xspXW 8oGhMyfKHFlpC/Tni5b17AL22r2v5XFe//uSJGh70zRLIk+xjg3YmW6/jfxbESTF dHdi8f5l08ALveuJ4I8sIMct42+HMkqiZVuIeg1IlyVzQv8FAZAuiGSMUlomNsLj HO9yk9Y6We7dVG9Fben0oAh7R4b+ZqyWy0rbh8SJeu3v+CT/hLmO/Ag0xo3zBv+X wgB5RYRhr7fVCEUMOkUO7yYxvlt/r+HRzMgV9lTIlVF9UZCQIpluvVWWiE9DXpbR lYVRy+FTaQmDKJBO3+QBR5jEzI5EKUuRFeBdzT0SBudBdE3r3AZl5uOwzTcSqLAp mryAdW+/vZAlS2tJABEBAAHCw7IEGAEKACYWIQRnnx7pKxlgnegW/egdsZj5NSXs GgUCaSxelAIbAgUJAeEzgAJACRAdsZj5NSXsGsF0IAQZAQoAHRYhBNFDbA26zqSH Aq+Xw2Px3XdTuLMVBQJpLF6UAAoJEGPx3XdTuLMVX2gP/jxQKk8JHjosAhpd1eyv /99x255Fx46KNpSTus2VKg0ffB+kDKqN5plbPExa6MEC/oxnGBio14ennnYLOIap ecx3WBJYh59wsjlDUwgOLsA+tfzyo6nBW/UIgYVZEATNkhbCuw3lQFZHW5e+be+K xuGBZqDH5IKJGL3XodyVNesND1phusLhMco34zVVc5LvRyJ5CzZgjXTIOzx1qbX0 uJalpYsd4LnChQNuzpRv31zO1d8FG5kE39osU6VNJh7fnDCnbXuj3FvZnPos26rT HNafdg+e+/8dOOvwt4UJ2tFAlLhiF/wQxFixhz1b4zXEOYRKuRn1XKm6XDaAT1Kp /zlmTQ2F0ObftNjaO9l6mTlunGey5CHUAZ2tAsdDwYIqewIrjjF0o+geMjlKmflI h5gkkcxNDXEagjkJXMzL8pzs8+g1B1cg1NHL9hsjp5VqhLD10J48n6C3nhwP1noB eM3GtKtcK6gK6IYxn3Fou7ABCbRhQXQVAci7iPJ4nW2ySQeQmeLl4lGxWPWZct9I LtH8ly+m2+7h7srMwDjRPeA3Owc7JgdLXdWdG7F4zppW4JqWvXIJ3wAJ+KFof6p6 WrEPWfHVM1qieTdchenPufH3CA474vYH/+l4ie+URXT9v2gukJ4LMVfrTxqJhUPw S4k6EOF/2sAi3EcXKDF4o+iEkqUQALGe/o8fng1biW6wD4z2fDu8zp3iU4ldNpst QMTX4QsTzsZee6lQoQQ6G+m3IOgQo7EKFEV6rxpMamyQbg2YW8WfypowlfGXAAJ8 x5mm+tdN4D6uvJidX2MyKAiAspP12Jc2T+Bap+e5TLi955Lk/RzcXgn7MUAZOD74 HND3/SFOg6mrZT1FPj4Qe5bArjONogYJoHo4/sNJMxKJ2g7WrBAILpgQBAHUts1f B+AZoShaFZ9UqAhnspzvI03XU9PUhPN50djdv1NachiRo6KNKCadvdWBo1ZAZsos cWLLFnx4miVcgRrmrErkQ/8tidtT3zRcEQE0DvWUat7nXJ7VVwbafhbTwA9v8O+U 3b9pDjlPBcy3hN3NRd8043XVQOzhTxPFwAof6g6ChqT6ANrZ8JN8DyKwUKCje8SX r96otE0K2jEtvJiqXeFTNxMlJ9aO2kdwpnN+58fYAp6FGwXtIU7pNAOCEfaCTmo1 LMuvW4WmjH6uvRlw8erhrUzqbikEzMwsxec02iZjfC8fhBBJxOZMDvPArjDKsYjl uBxYw6MBaBH4mkLQJWHubPQ6lUSEzEEtz76vfCQijh6JTI4kKH3MQfug11d3+NfP wfUCvYAaXxJlaY2kt9zY8GzUOy8AUuQN48cIlm70lGYYBE6lT+xgKqQMJ4Nn+m4t decWOnlnzsFNBGCGrk4BEACTD/+Nk/tDzN3viBmw0GvgWWyeyfVKuhXTYgp1NA2Z ugcsz9ZFjzQegH+jwekWc4JFSQTFHpxqog94eQ7UKzk3LaYeCMiPpuxyxsY8MSZo oAOcysRabkvVHNLFhCKiiTu7E8NkOlCT9v2+f/1aatFnM+D///1/RTR0MJ7lz3Eu QWtC6gC0MQBydHoN9Ofov07j8RSVXBBf7TfZjl+uYfpYEkP5++bnWLw1WMv8Acea XyCjoJ/3L5GfrIHoNmpRujj8FLAZV0YOdpQCEwMn6gfJrcWXcPLcg3vmmYLhOWqj 9kZoqE7Npejtzp9S4Yi9wM0ZTG+TTk2zec7dw7RstxTLEEJ8dx9IyXAkoNf8etlC 9f9KuTnLK23lsi3cvjs58WzYxtl6MQS9x8U9QBlb86K8GMDYiwRrPyDusVvzwe0l Zgrt7SboQP5+hD+wY92tJde9JQbYSVcIQwgRGPZGYIZ+DEo5g4SWBVp/y+pFTVd2 dFmbu8D2RLunI+hy7zjBEXbdRCxhyI16/lGG5wecg6Y4N26w3trUHymeTdAPQ+5s wE9F2MTz1D/FQrrb/pGa/6FcgusLvAvTJNCK/NAQNWx9ZJ1/teGCO8n2vhPi2995 0id4V93HdLcCy2PBAL4ltAp4gCBjXXRXZuou2jC+syfB/o8kln0/1sblBVlheopM bQARAQABwsF2BCgBCgAgFiEEZ58e6SsZYJ3oFv3oHbGY+TUl7BoFAmksXlcCHQIA CgkQHbGY+TUl7Br/gQ//dL3MGWJo5mjTCsZ+GG/faFGtzO2k6CbwDQooH4fq4ZUf I3yEFWDqm7lrKRvt40MnYmP6wDyObjcRXbbHoyXTZriDfz88u4tayVxLXa/t2hVB 2WxUQ8pjobZrq2HXnRGyFZcQjaKhS1u6qKovp45nTuPgVHCr8d7tZYYnY5EGkNz9 zUokkCc9yJNuS6VftyEZ7Lbv7kVluAz48Q5lJ2RBBOPa+a6SEI/Vlz431ZUCxnz8 W/m6u4NgpvSFHjDvpr7N+NGNZM7tdjZy3HTG/k7vnxUqAYR2NNd/xXOFT6LUTuAK DlO4n08lPW+/DOlqynVJXamHjXvMKlMlVNRANb9C2xt9yEsIrl0+6jMM/IFdaONX B5uqDUciCgEYR032MAg7L88kgOC3pjUjNkOZQB6YColoRhmhKiA1f46AxLObUWVe XwDueyIbhPdFie91F02gGwvsXF+Gp4RmcbG1G98oCVMR5Qb/eklL1Xr4wr9geRaO R9mMX/L1HEWykMX/bmapa+fuXGlOxG+RnJuyFvUVnZmbqCyOmVCRSS55ykUyu5wf SoxqJrcmGclvlPvXBr6vmwtfLYUFbqudMULZAWqGI5TWxZlRQqEJmmAD3t5cHhWU IMP50VMrn8SuYMhviOkcKzdkB4qYjeebMbCLvWu9rhupeW4ysa3psWxSbE1Sa7fC w7IEGAEKACYWIQRnnx7pKxlgnegW/egdsZj5NSXsGgUCYIauTgIbAgUJCWYBgAJA CRAdsZj5NSXsGsF0IAQZAQoAHRYhBCsQQmd/2BkMe5/A3CFh1y59zUJYBQJghq5O AAoJECFh1y59zUJYd/YP/idnBZt7ClccnTBIf4xXqEfLY9kWU3Xk5B8iPd/piBhP JM5/kLqEi1FzxrD6TRP/clApBnqGX3wciUSN9PgGvX/vP2gPl4BfJVn7h9i7SsJ+ RzwZ+10eiVv/sp0Nl35Ie+2ToXSAKOR8reC7VSseYIKCIZ3d0OnrjpuaB+PRf8Zg BtrZjFOM5Us+xHx0gDSWuk94hraJsF98IIWkj3LeS7WG6CFVoTN8jMbGv8V/+GyY J4UenPw0yFIJvGa4BWaxPQBHf+zFs01tg5LIiZ1AFHhn95mnaYLi8L2xguqo4faT oPqisiXysjlHTAASzRfhShc0MqbQV3hM8ZsM2xezcIng2p9lsuIj7PBagh0tdc7R usNwSDKx9VhxsaaRpz6ecxTUtvqQZxVkrZCcdpHvwOcIjbyGwm55qSL5txnpUI7I pv9a5DYxWWI5fvAA/Vb7y4Rta76HYLw9BC+ktMAJ9+Hye5s0rTWfxtUZQqKewl7J Q+W/f14tWxB/8fqRTwzLiVQF25QFx+2SMAflZ0QDIJ09awrjQLD82xY7N1A3RI/H Oba/Jwr7GxZfejxUVL3W+/bBKnSkXadZPPbmM2ZhEcObpjhbfHerRc/CdiekJ9O4 bWSD6X/w9P4TJYFGTjk3UM6kA5JIJhBVvOOQb6bNO2xA/xwW+pN/olV5t0qCJNxG jP8QAJ0nQTG8RSEsx3yUduU2kEHVqTzvLfceH3dMTIxpcFvyiydXRwk2RkcubXqW pXpaRWbINBERPsKykIdgYYf98r8T4imyF8CBcIP5Qrth4nVYTEjw3NwIfrIyJn0m t9K/A/MQHfaXK7Fh1h4rpFwA5ehHLKtmpMe5s/m2Z0/3VI0Xo0Ls6xRX3jn5mWf6 O/hnve1dDwxMapCChQxrvvp7JBA7NYJcW6duC90sMZpU83SVT//ysOe6UOl1JSWM AcosfYhKBHRQBqOwhNCcUB6vMTmlDYf5KPgIYamaYoGwiTWv9ZaW2Zo0QWPpBvp5 Qi4dk/69y1XFnDwj73B9OLW4Nu1irVlivsNUVvhgP6zp8/4e1GgQQ4t87iQ5BBQT 5IYMfZFHEPvb+5gS67i5FeUxNJZ7Dk33tUiPWCEH+kwS4AoM5A5AqZTw9ZslDwQC adz7WfP3h3ZeHKrwUuTrYgV/jKlgI0N9+iDRIkMiqwvyFegBJuHKuWzD5p3aO7Rx N7xJOf101r7BtYfg8SZWrmWOP3OlhV7NjC3F0Y2Rnk1Yvo3769So4hdutmRo/BXv hquGBJz8qYrboUe6QwdrYF/ycAmX5SSfNKZws3vsF4A49i94TOMkX8COXxx2tLsF +iqdj/MS4Y81F1vz0NQPPIOvu1bQOEU27GDEm44+94lprE3g =1ETd -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingCandidatesFinderTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.List; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class BindingCandidatesFinderTests { @RegisterExtension static final SonarLintLogTester logTester = new SonarLintLogTester(); private ConfigurationRepository configRepository; private BindingClueProvider bindingClueProvider; private SonarProjectsCache sonarProjectsCache; private SonarLintCancelMonitor cancelMonitor; private BindingCandidatesFinder underTest; @BeforeEach void setUp() { configRepository = mock(ConfigurationRepository.class); bindingClueProvider = mock(BindingClueProvider.class); sonarProjectsCache = mock(SonarProjectsCache.class); cancelMonitor = mock(SonarLintCancelMonitor.class); underTest = new BindingCandidatesFinder(configRepository, bindingClueProvider, sonarProjectsCache); } @Test void should_mark_scope_as_shared_configuration_when_any_clue_has_shared_config_origin() { var scope = new ConfigurationScope("scope1", null, true, "Test Scope"); var sharedConfigClue = new BindingClueProvider.UnknownBindingClue("projectKey", BindingSuggestionOrigin.SHARED_CONFIGURATION); var propertiesFileClue = new BindingClueProvider.UnknownBindingClue("projectKey", BindingSuggestionOrigin.PROPERTIES_FILE); when(configRepository.getAllBindableUnboundScopes()).thenReturn(List.of(scope)); when(bindingClueProvider.collectBindingCluesWithConnections("scope1", Set.of("conn1"), cancelMonitor)) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(sharedConfigClue, Set.of("conn1")), new BindingClueProvider.BindingClueWithConnections(propertiesFileClue, Set.of("conn1")) )); var candidates = underTest.findConfigScopesToBind("conn1", "projectKey", cancelMonitor); assertThat(candidates).hasSize(1); var candidate = candidates.iterator().next(); assertThat(candidate.getConfigurationScope()).isEqualTo(scope); assertThat(candidate.getOrigin()).isEqualTo(BindingSuggestionOrigin.SHARED_CONFIGURATION); } @Test void should_not_mark_scope_as_shared_configuration_when_no_clue_has_shared_config_origin() { var scope = new ConfigurationScope("scope1", null, true, "Test Scope"); var propertiesFileClue = new BindingClueProvider.UnknownBindingClue("projectKey", BindingSuggestionOrigin.PROPERTIES_FILE); var remoteUrlClue = new BindingClueProvider.UnknownBindingClue("projectKey", BindingSuggestionOrigin.REMOTE_URL); when(configRepository.getAllBindableUnboundScopes()).thenReturn(List.of(scope)); when(bindingClueProvider.collectBindingCluesWithConnections("scope1", Set.of("conn1"), cancelMonitor)) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(propertiesFileClue, Set.of("conn1")), new BindingClueProvider.BindingClueWithConnections(remoteUrlClue, Set.of("conn1")) )); var candidates = underTest.findConfigScopesToBind("conn1", "projectKey", cancelMonitor); assertThat(candidates).hasSize(1); var candidate = candidates.iterator().next(); assertThat(candidate.getConfigurationScope()).isEqualTo(scope); assertThat(candidate.getOrigin()).isEqualTo(BindingSuggestionOrigin.PROPERTIES_FILE); } @Test void should_select_project_name_when_name_matches_and_no_shared_or_properties_file_clues() { var scope = new ConfigurationScope("scope1", null, true, "MyProj"); when(configRepository.getAllBindableUnboundScopes()).thenReturn(List.of(scope)); when(bindingClueProvider.collectBindingCluesWithConnections("scope1", Set.of("conn1"), cancelMonitor)) .thenReturn(List.of( )); when(sonarProjectsCache.getSonarProject("conn1", "projectKey", cancelMonitor)) .thenReturn(Optional.of(new ServerProject("projectKey", "MyProj", false))); var candidates = underTest.findConfigScopesToBind("conn1", "projectKey", cancelMonitor); assertThat(candidates).hasSize(1); var candidate = candidates.iterator().next(); assertThat(candidate.getConfigurationScope()).isEqualTo(scope); assertThat(candidate.getOrigin()).isEqualTo(BindingSuggestionOrigin.PROJECT_NAME); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingClueProviderTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class BindingClueProviderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); public static final String SQ_CONNECTION_ID_1 = "sq1"; public static final String SQ_CONNECTION_ID_2 = "sq2"; public static final String SC_CONNECTION_ID_1 = "sc1"; public static final String SC_CONNECTION_ID_2 = "sc2"; private static final String PROJECT_KEY_1 = "myproject1"; public static final String MY_ORG_1 = "myOrg1"; public static final String MY_ORG_2 = "myOrg2"; public static final String CONFIG_SCOPE_ID = "configScopeId"; private final ConnectionConfigurationRepository connectionRepository = mock(ConnectionConfigurationRepository.class); private final ClientFileSystemService clientFs = mock(ClientFileSystemService.class); BindingClueProvider underTest = new BindingClueProvider(connectionRepository, clientFs, SonarCloudActiveEnvironment.prod()); @Test void should_detect_sonar_scanner_for_sonarqube() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=http://mysonarqube.org\n"))); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarQubeBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isNull(); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SQ_CONNECTION_ID_1); } @Test void should_detect_sonar_scanner_for_sonarqube_with_project_key() { mockFindFileByNamesInScope( List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=http://mysonarqube.org\nsonar.projectKey=" + PROJECT_KEY_1))); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); } @Test void should_match_multiple_connections() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=http://mysonarqube.org\n"))); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_2)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_2, "http://Mysonarqube.org/", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1, SQ_CONNECTION_ID_2), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SQ_CONNECTION_ID_1, SQ_CONNECTION_ID_2); } @Test void should_detect_sonar_scanner_for_sonarcloud_based_on_url() { mockFindFileByNamesInScope( List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=https://sonarcloud.io\nsonar.projectKey=" + PROJECT_KEY_1))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SC_CONNECTION_ID_2), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarCloudBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_1, SC_CONNECTION_ID_2); } @Test void should_detect_sonar_scanner_for_sonarcloud_based_on_url_and_region() { mockFindFileByNamesInScope( List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=https://sonarcloud.io\nsonar.projectKey=" + PROJECT_KEY_1 + "\nsonar.region=US"))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.US.getProductionUri(), SonarCloudRegion.US.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.US, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarCloudBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_1); assertThat(bindingClueWithConnections1.getBindingClue().getClass()).isEqualTo(BindingClueProvider.SonarCloudBindingClue.class); var sonarCloudBindingClue = (BindingClueProvider.SonarCloudBindingClue) bindingClueWithConnections1.getBindingClue(); assertThat(sonarCloudBindingClue.getRegion().name()).isEqualTo(SonarCloudRegion.US.name()); } @Test void should_detect_sonar_scanner_for_sonarcloud_based_on_organization() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.organization=" + MY_ORG_2))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_2)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_2, MY_ORG_2, SonarCloudRegion.EU, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SC_CONNECTION_ID_2), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarCloudBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isNull(); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_2); } @Test void should_detect_autoscan_for_sonarcloud() { mockFindFileByNamesInScope(List.of(buildClientFile(".sonarcloud.properties", "path/to/.sonarcloud.properties", "sonar.projectKey=" + PROJECT_KEY_1))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarCloudBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_1); } @Test void should_detect_autoscan_for_sonarcloud_and_region() { mockFindFileByNamesInScope(List.of(buildClientFile(".sonarcloud.properties", "path/to/.sonarcloud.properties", "sonar.projectKey=" + PROJECT_KEY_1 + "\nsonar.region=US"))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.US.getProductionUri(), SonarCloudRegion.US.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.US, true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.SonarCloudBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_1); assertThat(bindingClueWithConnections1.getBindingClue().getClass()).isEqualTo(BindingClueProvider.SonarCloudBindingClue.class); var sonarCloudBindingClue = (BindingClueProvider.SonarCloudBindingClue) bindingClueWithConnections1.getBindingClue(); assertThat(sonarCloudBindingClue.getRegion().name()).isEqualTo(SonarCloudRegion.US.name()); } @Test void should_detect_unknown_with_project_key() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.projectKey=" + PROJECT_KEY_1))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).hasSize(1); var bindingClueWithConnections1 = bindingClueWithConnections.get(0); assertThat(bindingClueWithConnections1.getBindingClue()).isInstanceOf(BindingClueProvider.UnknownBindingClue.class); assertThat(bindingClueWithConnections1.getBindingClue().getSonarProjectKey()).isEqualTo(PROJECT_KEY_1); assertThat(bindingClueWithConnections1.getConnectionIds()).containsOnly(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1); } @Test void ignore_scanner_file_without_clue() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.sources=src"))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).isEmpty(); } @Test void ignore_scanner_file_invalid_content() { mockFindFileByNamesInScope(List.of(buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "\\usonar.projectKey=" + PROJECT_KEY_1))); when(connectionRepository.getConnectionById(SC_CONNECTION_ID_1)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID_1, MY_ORG_1, SonarCloudRegion.EU, true)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SC_CONNECTION_ID_1, SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).isEmpty(); assertThat(logTester.logs(LogOutput.Level.ERROR)).contains("Unable to parse content of file 'file://path/to/sonar-project.properties'"); } @Test void should_not_detect_sonarlint_configuration_file_if_wrong_content() { mockFindSonarlintConfigurationFilesByScope(List.of(buildClientFile("connectedMode.json", "/path/to/.sonarlint/connectedMode.json", "{\"sonarCloudOrganization\": \"org\",\"sonarQubeUri\": \"http://mysonarqube.org\"}"))); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).isEmpty(); } @Test void should_not_detect_sonarlint_configuration_file_if_not_in_right_folder() { mockFindSonarlintConfigurationFilesByScope(List.of(buildClientFile("connectedMode.json", "/path/to/connections/connectedMode.json", "{\"projectKey\": \"pKey\",\"sonarQubeUri\": \"http://mysonarqube.org\"}"))); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var bindingClueWithConnections = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(bindingClueWithConnections).isEmpty(); } @Test void should_not_detect_sonarlint_configuration_file_if_not_json() { var file = new ClientFile(URI.create("/path/to/.sonarlint/connectedMode.txt"), CONFIG_SCOPE_ID, Path.of("/path/to/.sonarlint/connectedMode.txt"), false, null, null, null, true); assertThat(file.isSonarlintConfigurationFile()).isFalse(); } @Test void should_not_detect_sonarlint_configuration_file_if_wrong_folder() { var file = new ClientFile(URI.create("/path/to/.sonarlint/connectedMode.json"), CONFIG_SCOPE_ID, Path.of("/path/to/.sonarlint2/connectedMode.json"), false, null, null, null, true); assertThat(file.isSonarlintConfigurationFile()).isFalse(); } @Test void should_set_origin_properties_file_when_clue_created_from_properties() { mockFindFileByNamesInScope(List.of( buildClientFile("sonar-project.properties", "path/to/sonar-project.properties", "sonar.host.url=http://mysonarqube.org\nsonar.projectKey=" + PROJECT_KEY_1) )); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var cluesWithConn = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(cluesWithConn).hasSize(1); var clue = cluesWithConn.get(0).getBindingClue(); assertThat(clue.getOrigin()).isEqualTo(BindingSuggestionOrigin.PROPERTIES_FILE); } @Test void should_set_origin_shared_configuration_when_clue_created_from_shared_config() { // Simulate a shared configuration file var file = new ClientFile(URI.create("file:///path/to/.sonarlint/connectedMode.json"), CONFIG_SCOPE_ID, Paths.get("/path/to/.sonarlint/connectedMode.json"), false, null, null, null, true); file.setDirty("{\"projectKey\": \"" + PROJECT_KEY_1 + "\", \"sonarQubeUri\": \"http://mysonarqube.org\"}"); mockFindSonarlintConfigurationFilesByScope(List.of(file)); when(connectionRepository.getConnectionById(SQ_CONNECTION_ID_1)).thenReturn(new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_1, "http://mysonarqube.org", true)); var cluesWithConn = underTest.collectBindingCluesWithConnections(CONFIG_SCOPE_ID, Set.of(SQ_CONNECTION_ID_1), new SonarLintCancelMonitor()); assertThat(cluesWithConn).hasSize(1); var clue = cluesWithConn.get(0).getBindingClue(); assertThat(clue.getOrigin()).isEqualTo(BindingSuggestionOrigin.SHARED_CONFIGURATION); } private ClientFile buildClientFile(String filename, String relativePath, String content) { var file = new ClientFile(URI.create("file://" + relativePath), CONFIG_SCOPE_ID, Paths.get(relativePath), false, null, null, null, true); file.setDirty(content); return file; } private void mockFindFileByNamesInScope(List files) { when(clientFs.findFilesByNamesInScope(any(), any())).thenReturn(files); } private void mockFindSonarlintConfigurationFilesByScope(List files) { when(clientFs.findSonarlintConfigurationFilesByScope(any())).thenReturn(files); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/BindingSuggestionProviderTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import com.google.common.collect.ImmutableSortedSet; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.util.git.GitService; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.SuggestBindingParams; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.PROJECT_NAME; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionOrigin.REMOTE_URL; class BindingSuggestionProviderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); public static final String SQ_1_ID = "sq1"; public static final String SC_1_ID = "sc1"; public static final String SQ_2_ID = "sq2"; public static final SonarQubeConnectionConfiguration SQ_1 = new SonarQubeConnectionConfiguration(SQ_1_ID, "http://mysonarqube.com", true); public static final SonarCloudConnectionConfiguration SC_1 = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_1_ID, "myorg", SonarCloudRegion.EU, true); public static final String CONFIG_SCOPE_ID_1 = "configScope1"; public static final String PROJECT_KEY_1 = "projectKey1"; public static final ServerProject SERVER_PROJECT_1 = serverProject(PROJECT_KEY_1, "Project 1"); private final ConfigurationRepository configRepository = mock(ConfigurationRepository.class); private final ConnectionConfigurationRepository connectionRepository = mock(ConnectionConfigurationRepository.class); private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); private final BindingClueProvider bindingClueProvider = mock(BindingClueProvider.class); private final SonarProjectsCache sonarProjectsCache = mock(SonarProjectsCache.class); private final SonarQubeClientManager sonarQubeClientManager = mock(SonarQubeClientManager.class); private final ClientFileSystemService clientFs = mock(ClientFileSystemService.class); private final TelemetryService telemetryService = mock(TelemetryService.class); private final BindingSuggestionProvider underTest = new BindingSuggestionProvider(configRepository, connectionRepository, client, bindingClueProvider, sonarProjectsCache, sonarQubeClientManager, clientFs, telemetryService); @BeforeEach void setup() { when(sonarProjectsCache.getTextSearchIndex(anyString(), any(SonarLintCancelMonitor.class))).thenReturn(new TextSearchIndex<>()); logTester.clear(); } @Test void trigger_suggest_binding_if_config_flag_turned_on() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID_1, BindingConfiguration.noBinding(true), BindingConfiguration.noBinding())); assertThat(logTester.logs(LogOutput.Level.DEBUG)).contains("Binding suggestion computation queued for config scopes '" + CONFIG_SCOPE_ID_1 + "'..."); } @Test void dont_trigger_suggest_binding_if_config_flag_turned_off() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID_1, BindingConfiguration.noBinding(), BindingConfiguration.noBinding(true))); assertThat(logTester.logs()).isEmpty(); } @Test void trigger_suggest_binding_if_connection_added_and_at_least_one_config_scope() { when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigScopeIds()).thenReturn(Set.of("id1")); underTest.connectionAdded(new ConnectionConfigurationAddedEvent(SQ_1_ID, ConnectionKind.SONARQUBE)); assertThat(logTester.logs(LogOutput.Level.DEBUG)).contains("Binding suggestions computation queued for connection '" + SQ_1_ID + "'..."); } @Test void dont_trigger_suggest_binding_if_connection_added_but_no_config_scopes() { when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigScopeIds()).thenReturn(Set.of()); underTest.connectionAdded(new ConnectionConfigurationAddedEvent(SQ_1_ID, ConnectionKind.SONARQUBE)); assertThat(logTester.logs()).isEmpty(); } @Test void dont_trigger_suggest_binding_if_connection_added_but_then_gone() { when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(null); underTest.connectionAdded(new ConnectionConfigurationAddedEvent(SQ_1_ID, ConnectionKind.SONARQUBE)); assertThat(logTester.logs(LogOutput.Level.DEBUG)).isEmpty(); } @Test void skip_suggestions_for_non_eligible_config_scopes() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope("configScopeWithNoBinding")).thenReturn(new ConfigurationScope("configScopeWithNoBinding", null, true, "Binding gone!")); when(configRepository.getBindingConfiguration("configScopeWithNoConfig")).thenReturn(BindingConfiguration.noBinding()); when(configRepository.getConfigurationScope("configScopeNotBindable")).thenReturn(new ConfigurationScope("configScopeNotBindable", null, false, "Not bindable")); when(configRepository.getBindingConfiguration("configScopeNotBindable")).thenReturn(BindingConfiguration.noBinding()); when(configRepository.getConfigurationScope("alreadyBound")).thenReturn(new ConfigurationScope("alreadyBound", null, true, "Already bound")); when(configRepository.getBindingConfiguration("alreadyBound")).thenReturn(new BindingConfiguration(SQ_1_ID, PROJECT_KEY_1, false)); when(configRepository.getConfigurationScope("suggestionsDisabled")).thenReturn(new ConfigurationScope("suggestionsDisabled", null, true, "Suggestion disabled")); when(configRepository.getBindingConfiguration("suggestionsDisabled")).thenReturn(BindingConfiguration.noBinding(true)); underTest.suggestBindingForGivenScopesAndAllConnections(ImmutableSortedSet.of("configScopeWithNoBinding", "configScopeWithNoConfig", "configScopeNotBindable", "alreadyBound", "suggestionsDisabled")); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .contains( "Configuration scope 'configScopeWithNoBinding' is gone.", "Configuration scope 'configScopeWithNoConfig' is gone.", "Configuration scope 'configScopeNotBindable' is not bindable.", "Configuration scope 'alreadyBound' is already bound.", "Configuration scope 'suggestionsDisabled' has binding suggestions disabled.")); } @Test void compute_suggestions_for_config_scope_with_invalid_binding() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope("brokenBinding1")).thenReturn(new ConfigurationScope("brokenBinding1", null, true, "Already bound")); when(configRepository.getBindingConfiguration("brokenBinding1")).thenReturn(new BindingConfiguration(null, PROJECT_KEY_1, false)); when(configRepository.getConfigurationScope("brokenBinding2")).thenReturn(new ConfigurationScope("brokenBinding2", null, true, "Already bound")); when(configRepository.getBindingConfiguration("brokenBinding2")).thenReturn(new BindingConfiguration(SQ_1_ID, null, false)); when(configRepository.getConfigurationScope("connectionGone")).thenReturn(new ConfigurationScope("connectionGone", null, true, "Already bound")); when(configRepository.getBindingConfiguration("connectionGone")).thenReturn(new BindingConfiguration(SQ_2_ID, PROJECT_KEY_1, false)); underTest.suggestBindingForGivenScopesAndAllConnections(ImmutableSortedSet.of("brokenBinding1", "brokenBinding2", "connectionGone")); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .contains( "Found 0 suggestions for configuration scope 'brokenBinding1'", "Found 0 suggestions for configuration scope 'brokenBinding2'", "Found 0 suggestions for configuration scope 'connectionGone'")); } @Test void compute_suggestions_favor_search_by_project_key() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Config scope")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of(new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID)))); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.of(SERVER_PROJECT_1)); underTest.suggestBindingForGivenScopesAndAllConnections(Set.of(CONFIG_SCOPE_ID_1)); var captor = ArgumentCaptor.forClass(SuggestBindingParams.class); verify(client, timeout(1000)).suggestBinding(captor.capture()); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsExactly( "Binding suggestion computation queued for config scopes '" + CONFIG_SCOPE_ID_1 + "'...", "Found 1 suggestion for configuration scope '" + CONFIG_SCOPE_ID_1 + "'"); verify(sonarProjectsCache, never()).getTextSearchIndex(anyString(), any(SonarLintCancelMonitor.class)); var params = captor.getValue(); assertThat(params.getSuggestions()).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(params.getSuggestions().get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly(tuple(SQ_1_ID, PROJECT_KEY_1, "Project 1")); } @Test void compute_suggestions_fallback_to_text_search_all_connections_if_no_matches_by_projectKey() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1, SC_1_ID, SC_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(connectionRepository.getConnectionById(SC_1_ID)).thenReturn(SC_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "KEYWORD")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of(new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID)))); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); when(sonarProjectsCache.getSonarProject(eq(SC_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar keyword"); when(sonarProjectsCache.getTextSearchIndex(eq(SC_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); underTest.suggestBindingForGivenScopesAndAllConnections(Set.of(CONFIG_SCOPE_ID_1)); var captor = ArgumentCaptor.forClass(SuggestBindingParams.class); verify(client, timeout(1000)).suggestBinding(captor.capture()); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsExactlyInAnyOrder( "Binding suggestion computation queued for config scopes '" + CONFIG_SCOPE_ID_1 + "'...", "Attempt to find a good match for 'KEYWORD' on connection '" + SQ_1_ID + "'...", "Attempt to find a good match for 'KEYWORD' on connection '" + SC_1_ID + "'...", "Best score = 0.33", "Found 1 suggestion for configuration scope '" + CONFIG_SCOPE_ID_1 + "'"); var params = captor.getValue(); assertThat(params.getSuggestions()).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(params.getSuggestions().get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly(tuple(SC_1_ID, PROJECT_KEY_1, "Project 1")); } @Test void compute_suggestions_fallback_to_text_search_all_connections_if_no_matches_by_projectKey_and_no_other_clue() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1, SC_1_ID, SC_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(connectionRepository.getConnectionById(SC_1_ID)).thenReturn(SC_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "KEYWORD")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of(new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID)))); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); when(sonarProjectsCache.getSonarProject(eq(SC_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar keyword"); when(sonarProjectsCache.getTextSearchIndex(eq(SC_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); underTest.suggestBindingForGivenScopesAndAllConnections(Set.of(CONFIG_SCOPE_ID_1)); var captor = ArgumentCaptor.forClass(SuggestBindingParams.class); verify(client, timeout(1000)).suggestBinding(captor.capture()); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsExactlyInAnyOrder( "Binding suggestion computation queued for config scopes '" + CONFIG_SCOPE_ID_1 + "'...", "Attempt to find a good match for 'KEYWORD' on connection '" + SQ_1_ID + "'...", "Attempt to find a good match for 'KEYWORD' on connection '" + SC_1_ID + "'...", "Best score = 0.33", "Found 1 suggestion for configuration scope '" + CONFIG_SCOPE_ID_1 + "'"); var params = captor.getValue(); assertThat(params.getSuggestions()).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(params.getSuggestions().get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly(tuple(SC_1_ID, PROJECT_KEY_1, "Project 1")); } @Test void get_suggested_binding() { var cancelMonitor = new SonarLintCancelMonitor(); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "foo-bar")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(CONFIG_SCOPE_ID_1, Set.of(SQ_1_ID), cancelMonitor)) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.SonarQubeBindingClue(null, null, PROJECT_NAME), Set.of(SQ_1_ID)))); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar garbage1"); when(sonarProjectsCache.getTextSearchIndex(SQ_1_ID, cancelMonitor)).thenReturn(searchIndex); when(sonarProjectsCache.getSonarProject(SQ_1_ID, PROJECT_KEY_1, cancelMonitor)).thenReturn(Optional.empty()); var bindingSuggestions = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); assertThat(bindingSuggestions).hasSize(1); assertThat(bindingSuggestions.get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly( tuple(SQ_1_ID, PROJECT_KEY_1, "Project 1")); } @Test void search_only_among_connection_candidates() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1, SC_1_ID, SC_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(connectionRepository.getConnectionById(SC_1_ID)).thenReturn(SC_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "foo-bar")); when(configRepository.getConfigScopeIds()).thenReturn(Set.of(CONFIG_SCOPE_ID_1)); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID, SC_1_ID)))); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); when(sonarProjectsCache.getSonarProject(eq(SC_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar garbage1"); searchIndex.index(serverProject("key2", "Project 2"), "foo bar garbage2"); searchIndex.index(serverProject("key3", "Project 3"), "foo bar more garbage"); when(sonarProjectsCache.getTextSearchIndex(eq(SC_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); when(sonarProjectsCache.getTextSearchIndex(eq(SQ_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); underTest.connectionAdded(new ConnectionConfigurationAddedEvent(SQ_1_ID, ConnectionKind.SONARQUBE)); var captor = ArgumentCaptor.forClass(SuggestBindingParams.class); verify(client, timeout(1000)).suggestBinding(captor.capture()); var params = captor.getValue(); assertThat(params.getSuggestions()).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(params.getSuggestions().get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly( tuple(SQ_1_ID, PROJECT_KEY_1, "Project 1"), tuple(SQ_1_ID, "key2", "Project 2")); } @Test void text_search_should_retain_only_top_scores() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "foo-bar")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID, SC_1_ID)), new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.SonarCloudBindingClue(null, null, null, PROJECT_NAME), Set.of(SC_1_ID)))); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); when(sonarProjectsCache.getSonarProject(eq(SC_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar garbage1"); searchIndex.index(serverProject("key2", "Project 2"), "foo bar garbage2"); searchIndex.index(serverProject("key3", "Project 3"), "foo bar more garbage"); when(sonarProjectsCache.getTextSearchIndex(eq(SC_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); underTest.suggestBindingForGivenScopesAndAllConnections(Set.of(CONFIG_SCOPE_ID_1)); var captor = ArgumentCaptor.forClass(SuggestBindingParams.class); verify(client, timeout(1000)).suggestBinding(captor.capture()); assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsExactly( "Binding suggestion computation queued for config scopes '" + CONFIG_SCOPE_ID_1 + "'...", "Attempt to find a good match for 'foo-bar' on connection '" + SC_1_ID + "'...", "Best score = 0.67", "Found 2 suggestions for configuration scope '" + CONFIG_SCOPE_ID_1 + "'"); var params = captor.getValue(); assertThat(params.getSuggestions()).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(params.getSuggestions().get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly( tuple(SC_1_ID, PROJECT_KEY_1, "Project 1"), tuple(SC_1_ID, "key2", "Project 2")); } @Test void search_by_remote_url_should_return_suggestion_when_project_found_for_sonarcloud() { var cancelMonitor = new SonarLintCancelMonitor(); var baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SC_1_ID, SC_1)); when(connectionRepository.getConnectionById(SC_1_ID)).thenReturn(SC_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SC_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn("git@github.com:myorg/myproj.git"); when(sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(eq(SC_1_ID), any())) .thenReturn(Optional.of(new BindingSuggestionDto(SC_1_ID, PROJECT_KEY_1, "Project 1", REMOTE_URL))); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SC_1_ID, cancelMonitor); assertThat(result).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(result.get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly(tuple(SC_1_ID, PROJECT_KEY_1, "Project 1")); } } @Test void search_by_remote_url_should_return_suggestion_when_project_found() { var cancelMonitor = new SonarLintCancelMonitor(); Path baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn("git@github.com:myorg/myproj.git"); when(sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(eq(SQ_1_ID), any())) .thenReturn(Optional.of(new BindingSuggestionDto(SQ_1_ID, PROJECT_KEY_1, "Project 1", REMOTE_URL))); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); assertThat(result).containsOnlyKeys(CONFIG_SCOPE_ID_1); assertThat(result.get(CONFIG_SCOPE_ID_1)) .extracting(BindingSuggestionDto::getConnectionId, BindingSuggestionDto::getSonarProjectKey, BindingSuggestionDto::getSonarProjectName) .containsOnly(tuple(SQ_1_ID, PROJECT_KEY_1, "Project 1")); } } @Test void should_set_origin_remote_url_on_remote_url_based_suggestion() { var cancelMonitor = new SonarLintCancelMonitor(); Path baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn("git@github.com:myorg/myproj.git"); when(sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(eq(SQ_1_ID), any())) .thenReturn(Optional.of(new BindingSuggestionDto(SQ_1_ID, PROJECT_KEY_1, "Project 1", REMOTE_URL))); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); var suggestions = result.get(CONFIG_SCOPE_ID_1); assertThat(suggestions).hasSize(1); assertThat(suggestions.get(0).getOrigin()).isEqualTo(REMOTE_URL); } } @Test void legacy_isFromSharedConfiguration_boolean_remains_false_for_remote_url() { var cancelMonitor = new SonarLintCancelMonitor(); Path baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn("git@github.com:myorg/myproj.git"); when(sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(eq(SQ_1_ID), any())) .thenReturn(Optional.of(new BindingSuggestionDto(SQ_1_ID, PROJECT_KEY_1, "Project 1", REMOTE_URL))); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); var suggestions = result.get(CONFIG_SCOPE_ID_1); assertThat(suggestions).hasSize(1); assertThat(suggestions.get(0).isFromSharedConfiguration()).isFalse(); } } @Test void should_set_origin_project_name_on_project_name_based_suggestion() { when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "foo-bar")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of( new BindingClueProvider.BindingClueWithConnections(new BindingClueProvider.UnknownBindingClue(PROJECT_KEY_1, PROJECT_NAME), Set.of(SQ_1_ID)))); var searchIndex = new TextSearchIndex(); searchIndex.index(SERVER_PROJECT_1, "foo bar garbage1"); when(sonarProjectsCache.getTextSearchIndex(eq(SQ_1_ID), any(SonarLintCancelMonitor.class))).thenReturn(searchIndex); when(sonarProjectsCache.getSonarProject(eq(SQ_1_ID), eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))).thenReturn(Optional.empty()); var suggestionsMap = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, new SonarLintCancelMonitor()); var suggestions = suggestionsMap.get(CONFIG_SCOPE_ID_1); assertThat(suggestions).isNotEmpty(); assertThat(suggestions) .extracting(BindingSuggestionDto::getOrigin) .containsOnly(PROJECT_NAME); } @Test void search_by_remote_url_should_do_nothing_when_no_remote_url() { var cancelMonitor = new SonarLintCancelMonitor(); var baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn(null); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); assertThat(result).isEmpty(); } } @Test void search_by_remote_url_should_do_nothing_when_no_project_id_found() { var cancelMonitor = new SonarLintCancelMonitor(); var baseDir = Path.of("repo"); when(connectionRepository.getConnectionsById()).thenReturn(Map.of(SQ_1_ID, SQ_1)); when(connectionRepository.getConnectionById(SQ_1_ID)).thenReturn(SQ_1); when(configRepository.getConfigurationScope(CONFIG_SCOPE_ID_1)) .thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID_1, null, true, "Some project")); when(configRepository.getBindingConfiguration(CONFIG_SCOPE_ID_1)).thenReturn(BindingConfiguration.noBinding()); when(bindingClueProvider.collectBindingCluesWithConnections(eq(CONFIG_SCOPE_ID_1), eq(Set.of(SQ_1_ID)), any(SonarLintCancelMonitor.class))) .thenReturn(List.of()); when(clientFs.getBaseDir(CONFIG_SCOPE_ID_1)).thenReturn(baseDir); try (var gitServiceMock = mockStatic(GitService.class)) { gitServiceMock.when(() -> GitService.getRemoteUrl(baseDir)).thenReturn("git@github.com:myorg/myproj.git"); when(sonarQubeClientManager.withActiveClientFlatMapOptionalAndReturn(eq(SQ_1_ID), any())) .thenReturn(Optional.empty()); var result = underTest.getBindingSuggestions(CONFIG_SCOPE_ID_1, SQ_1_ID, cancelMonitor); assertThat(result).isEmpty(); } } private static ServerProject serverProject(String projectKey, String name) { return new ServerProject(projectKey, name, false); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/ConfigurationServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; class ConfigurationServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String CONNECTION_1 = "connection1"; private static final String CONNECTION_2 = "connection2"; public static final BindingConfigurationDto BINDING_DTO_1 = new BindingConfigurationDto(CONNECTION_1, "projectKey1", false); public static final BindingConfigurationDto BINDING_DTO_2 = new BindingConfigurationDto(CONNECTION_1, "projectKey2", true); public static final BindingConfigurationDto BINDING_DTO_3 = new BindingConfigurationDto(CONNECTION_2, "projectKey3", true); public static final ConfigurationScopeDto CONFIG_DTO_1 = new ConfigurationScopeDto("id1", null, true, "Scope 1", BINDING_DTO_1); public static final ConfigurationScopeDto CONFIG_DTO_1_DUP = new ConfigurationScopeDto("id1", null, false, "Scope 1 dup", BINDING_DTO_2); public static final ConfigurationScopeDto CONFIG_DTO_2 = new ConfigurationScopeDto("id2", null, true, "Scope 2", BINDING_DTO_2); public static final ConfigurationScopeDto CONFIG_DTO_3 = new ConfigurationScopeDto("id3", null, true, "Scope 2", BINDING_DTO_3); private final ConfigurationRepository repository = new ConfigurationRepository(); private ApplicationEventPublisher eventPublisher; private ConfigurationService underTest; @BeforeEach void setUp() { eventPublisher = mock(ApplicationEventPublisher.class); underTest = new ConfigurationService(eventPublisher, repository); } @Test void initialize_empty() { assertThat(repository.getConfigScopeIds()).isEmpty(); } @Test void get_binding_of_unknown_config_returns_null() { assertThat(repository.getBindingConfiguration("not_found")).isNull(); } @Test void add_configuration_should_post_event() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_2)); assertThat(repository.getConfigScopeIds()).containsOnly("id2"); assertThat(repository.getBindingConfiguration("id2")).usingRecursiveComparison().isEqualTo(BINDING_DTO_2); ArgumentCaptor captor = ArgumentCaptor.forClass(ConfigurationScopesAddedWithBindingEvent.class); verify(eventPublisher).publishEvent(captor.capture()); var event = captor.getValue(); assertThat(event.getConfigScopeIds()).containsOnly("id2"); } @Test void add_multiple_configurations_should_post_batch_event() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1, CONFIG_DTO_2)); assertThat(repository.getConfigScopeIds()).containsOnly("id1", "id2"); assertThat(repository.getBindingConfiguration("id1")).usingRecursiveComparison().isEqualTo(BINDING_DTO_1); assertThat(repository.getBindingConfiguration("id2")).usingRecursiveComparison().isEqualTo(BINDING_DTO_2); ArgumentCaptor captor = ArgumentCaptor.forClass(ConfigurationScopesAddedWithBindingEvent.class); verify(eventPublisher).publishEvent(captor.capture()); var event = captor.getValue(); assertThat(event.getConfigScopeIds()).containsOnly("id1", "id2"); } @Test void add_duplicate_should_log_and_update() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1)); assertThat(repository.getBindingConfiguration("id1")).usingRecursiveComparison().isEqualTo(BINDING_DTO_1); underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1_DUP)); assertThat(repository.getConfigScopeIds()).containsOnly("id1"); assertThat(repository.getBindingConfiguration("id1")).usingRecursiveComparison().isEqualTo(BINDING_DTO_2); assertThat(logTester.logs(LogOutput.Level.ERROR)).containsExactly("Duplicate configuration scope registered: id1"); } @Test void remove_configuration() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1)); assertThat(repository.getConfigScopeIds()).containsOnly("id1"); underTest.didRemoveConfigurationScope("id1"); assertThat(repository.getConfigScopeIds()).isEmpty(); } @Test void remove_unknown_configuration_should_log() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1)); assertThat(repository.getConfigScopeIds()).containsOnly("id1"); underTest.didRemoveConfigurationScope("id2"); assertThat(repository.getConfigScopeIds()).containsOnly("id1"); assertThat(logTester.logs(LogOutput.Level.DEBUG)).contains("Attempt to remove configuration scope 'id2' that was not registered"); assertThat(logTester.logs(LogOutput.Level.ERROR)).isEmpty(); } @Test void update_binding_config_and_post_event() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1)); assertThat(repository.getBindingConfiguration("id1")).usingRecursiveComparison().isEqualTo(BINDING_DTO_1); // Ignore add event Mockito.reset(eventPublisher); underTest.didUpdateBinding("id1", BINDING_DTO_2); assertThat(repository.getConfigScopeIds()).containsOnly("id1"); assertThat(repository.getBindingConfiguration("id1")).usingRecursiveComparison().isEqualTo(BINDING_DTO_2); ArgumentCaptor captor = ArgumentCaptor.forClass(BindingConfigChangedEvent.class); verify(eventPublisher).publishEvent(captor.capture()); var event = captor.getValue(); assertThat(event.configScopeId()).isEqualTo("id1"); assertThat(event.previousConfig().connectionId()).isEqualTo(CONNECTION_1); assertThat(event.previousConfig().sonarProjectKey()).isEqualTo("projectKey1"); assertThat(event.previousConfig().bindingSuggestionDisabled()).isFalse(); assertThat(event.newConfig().connectionId()).isEqualTo(CONNECTION_1); assertThat(event.newConfig().sonarProjectKey()).isEqualTo("projectKey2"); assertThat(event.newConfig().bindingSuggestionDisabled()).isTrue(); } @Test void update_binding_config_for_unknown_config_scope_should_log() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1)); underTest.didUpdateBinding("id2", BINDING_DTO_2); assertThat(logTester.logs(LogOutput.Level.ERROR)).containsExactly("Attempt to update binding in configuration scope 'id2' that was not registered"); } @Test void should_clear_binding_if_connection_removed() { underTest.didAddConfigurationScopes(List.of(CONFIG_DTO_1, CONFIG_DTO_3)); assertThat(repository.getConfigScopeIds()).containsOnly("id1", "id3"); underTest.connectionRemoved(new ConnectionConfigurationRemovedEvent(CONNECTION_1)); assertThat(repository.getAllBoundScopes()).hasSize(1); assertThat(repository.getBoundScope("id3")) .isNotNull() .extracting(BoundScope::getConnectionId).isEqualTo(CONNECTION_2); assertThat(repository.getBindingConfiguration(CONFIG_DTO_1.getId())) .extracting(BindingConfiguration::connectionId, BindingConfiguration::sonarProjectKey, BindingConfiguration::bindingSuggestionDisabled) .containsExactly(null, null, false); assertThat(repository.getBindingConfiguration(CONFIG_DTO_3.getId())) .extracting(BindingConfiguration::connectionId, BindingConfiguration::sonarProjectKey, BindingConfiguration::bindingSuggestionDisabled) .containsExactly(BINDING_DTO_3.getConnectionId(), BINDING_DTO_3.getSonarProjectKey(), BINDING_DTO_3.isBindingSuggestionDisabled()); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/ConnectionServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.List; import java.util.Map; import org.assertj.core.api.InstanceOfAssertFactories; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationAddedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class ConnectionServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final ConnectionConfigurationRepository repository = new ConnectionConfigurationRepository(); public static final SonarQubeConnectionConfigurationDto SQ_DTO_1 = new SonarQubeConnectionConfigurationDto("sq1", "http://url1/", true); public static final SonarQubeConnectionConfigurationDto SQ_DTO_1_DUP = new SonarQubeConnectionConfigurationDto("sq1", "http://url1_dup/", true); public static final SonarQubeConnectionConfigurationDto SQ_DTO_2 = new SonarQubeConnectionConfigurationDto("sq2", "url2", true); public static final SonarCloudConnectionConfigurationDto SC_DTO_1 = new SonarCloudConnectionConfigurationDto("sc1", "org1", org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, true); public static final SonarCloudConnectionConfigurationDto SC_DTO_2 = new SonarCloudConnectionConfigurationDto("sc2", "org2", org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, true); private static final String EXPECTED_MESSAGE = "UTM parameters should match regular expression: [a-z0-9\\-]+"; ApplicationEventPublisher eventPublisher; ConnectionService underTest; @BeforeEach void setUp() { eventPublisher = mock(ApplicationEventPublisher.class); } @Test void initialize_provide_connections() { underTest = new ConnectionService(eventPublisher, repository, List.of(SQ_DTO_1, SQ_DTO_2), List.of(SC_DTO_1, SC_DTO_2), SonarCloudActiveEnvironment.prod(), null, null); assertThat(repository.getConnectionsById()).containsOnlyKeys("sq1", "sq2", "sc1", "sc2"); } @Test void generate_user_token_should_ignore_null_utm() { SonarLintCancelMonitor cancelMonitor = new SonarLintCancelMonitor(); TokenGeneratorHelper mockedHelper = mock(TokenGeneratorHelper.class); when(mockedHelper.helpGenerateUserToken("serverUrl", null, cancelMonitor)) .thenReturn(new HelpGenerateUserTokenResponse("TOKEN")); underTest = new ConnectionService(eventPublisher, repository, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, mockedHelper); var response = underTest.helpGenerateUserToken("serverUrl", null, cancelMonitor); assertThat(response.getToken()).isEqualTo("TOKEN"); } @Test void generate_user_token_should_throw_validation_error_for_all() { TokenGeneratorHelper mockedHelper = mock(TokenGeneratorHelper.class); underTest = new ConnectionService(eventPublisher, repository, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, mockedHelper); HelpGenerateUserTokenParams.Utm invalidParams = new HelpGenerateUserTokenParams.Utm("medium wrong", "source/", "contENT", "t.e.r.m"); SonarLintCancelMonitor cancelMonitor = new SonarLintCancelMonitor(); ResponseErrorException exception = catchThrowableOfType(ResponseErrorException.class, () -> underTest.helpGenerateUserToken("serverUrl", invalidParams, cancelMonitor)); ResponseError innerError = exception.getResponseError(); assertThat(exception).hasMessage(EXPECTED_MESSAGE); assertThat(innerError).extracting("message").isEqualTo(EXPECTED_MESSAGE); assertThat(innerError).extracting("code").isEqualTo(ResponseErrorCode.InvalidParams.getValue()); assertThat(innerError).extracting("data").asInstanceOf(InstanceOfAssertFactories.array(String[].class)) .containsExactlyInAnyOrder("utm_medium", "utm_source", "utm_content", "utm_term"); verifyNoInteractions(mockedHelper); } @Test void generate_user_token_should_throw_validation_error_for_two() { TokenGeneratorHelper mockedHelper = mock(TokenGeneratorHelper.class); underTest = new ConnectionService(eventPublisher, repository, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, mockedHelper); HelpGenerateUserTokenParams.Utm invalidParams = new HelpGenerateUserTokenParams.Utm("medium wrong", "source", "cont-ent", "t.e.r.m"); SonarLintCancelMonitor cancelMonitor = new SonarLintCancelMonitor(); ResponseErrorException exception = catchThrowableOfType(ResponseErrorException.class, () -> underTest.helpGenerateUserToken("serverUrl", invalidParams, cancelMonitor)); ResponseError innerError = exception.getResponseError(); assertThat(exception).hasMessage(EXPECTED_MESSAGE); assertThat(innerError).extracting("message").isEqualTo(EXPECTED_MESSAGE); assertThat(innerError).extracting("code").isEqualTo(ResponseErrorCode.InvalidParams.getValue()); assertThat(innerError).extracting("data").asInstanceOf(InstanceOfAssertFactories.array(String[].class)) .containsExactlyInAnyOrder("utm_medium", "utm_term"); verifyNoInteractions(mockedHelper); } @Test void add_new_connection_and_post_event() { underTest = new ConnectionService(eventPublisher, repository, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, null); underTest.didUpdateConnections(List.of(SQ_DTO_1), List.of()); assertThat(repository.getConnectionsById()).containsOnlyKeys("sq1"); assertThat(repository.getConnectionById("sq1")) .asInstanceOf(InstanceOfAssertFactories.type(SonarQubeConnectionConfiguration.class)) .extracting(SonarQubeConnectionConfiguration::getConnectionId, SonarQubeConnectionConfiguration::getUrl, SonarQubeConnectionConfiguration::isDisableNotifications, SonarQubeConnectionConfiguration::getKind) .containsOnly("sq1", "http://url1", true, ConnectionKind.SONARQUBE); underTest.didUpdateConnections(List.of(SQ_DTO_1, SQ_DTO_2), List.of()); assertThat(repository.getConnectionsById()).containsOnlyKeys("sq1", "sq2"); underTest.didUpdateConnections(List.of(SQ_DTO_1, SQ_DTO_2), List.of(SC_DTO_1)); assertThat(repository.getConnectionsById()).containsOnlyKeys("sq1", "sq2", "sc1"); assertThat(repository.getConnectionById("sc1")) .asInstanceOf(InstanceOfAssertFactories.type(SonarCloudConnectionConfiguration.class)) .extracting(SonarCloudConnectionConfiguration::getConnectionId, SonarCloudConnectionConfiguration::getUrl, SonarCloudConnectionConfiguration::isDisableNotifications, SonarCloudConnectionConfiguration::getKind, SonarCloudConnectionConfiguration::getOrganization) .containsOnly("sc1", "https://sonarcloud.io", true, ConnectionKind.SONARCLOUD, "org1"); var captor = ArgumentCaptor.forClass(ConnectionConfigurationAddedEvent.class); verify(eventPublisher, times(3)).publishEvent(captor.capture()); var events = captor.getAllValues(); assertThat(events).extracting(ConnectionConfigurationAddedEvent::addedConnectionId).containsExactly("sq1", "sq2", "sc1"); } @Test void multiple_connections_with_same_id_should_log_and_ignore() { underTest = new ConnectionService(eventPublisher, repository, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, null); underTest.didUpdateConnections(List.of(SQ_DTO_1), List.of()); underTest.didUpdateConnections(List.of(SQ_DTO_1, SQ_DTO_1_DUP), List.of()); assertThat(repository.getConnectionById("sq1")) .asInstanceOf(InstanceOfAssertFactories.type(SonarQubeConnectionConfiguration.class)) .extracting(SonarQubeConnectionConfiguration::getConnectionId, SonarQubeConnectionConfiguration::getUrl, SonarQubeConnectionConfiguration::isDisableNotifications, SonarQubeConnectionConfiguration::getKind) .containsOnly("sq1", "http://url1_dup", true, ConnectionKind.SONARQUBE); assertThat(logTester.logs(LogOutput.Level.ERROR)).containsExactly("Duplicate connection registered: sq1"); } @Test void remove_connection() { underTest = new ConnectionService(eventPublisher, repository, List.of(SQ_DTO_1), List.of(SC_DTO_1), SonarCloudActiveEnvironment.prod(), null, null); assertThat(repository.getConnectionsById()).containsKeys("sq1", "sc1"); underTest.didUpdateConnections(List.of(SQ_DTO_1), List.of()); assertThat(repository.getConnectionsById()).containsKeys("sq1"); underTest.didUpdateConnections(List.of(), List.of()); assertThat(repository.getConnectionsById()).isEmpty(); var captor = ArgumentCaptor.forClass(ConnectionConfigurationRemovedEvent.class); verify(eventPublisher, times(2)).publishEvent(captor.capture()); var events = captor.getAllValues(); assertThat(events).extracting(ConnectionConfigurationRemovedEvent::removedConnectionId).containsExactly("sc1", "sq1"); } @Test void remove_connection_should_log_if_unknown_connection_and_ignore() { var mockedRepo = mock(ConnectionConfigurationRepository.class); underTest = new ConnectionService(eventPublisher, mockedRepo, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, null); // Emulate a race condition on the repository: the connection is gone between get and remove when(mockedRepo.getConnectionsById()).thenReturn(Map.of("id", new SonarQubeConnectionConfiguration("id", "http://foo", true))); when(mockedRepo.remove("id")).thenReturn(null); underTest.didUpdateConnections(List.of(), List.of()); assertThat(logTester.logs(LogOutput.Level.DEBUG)).containsExactly("Attempt to remove connection 'id' that was not registered. Possibly a race condition?"); } @Test void update_connection() { underTest = new ConnectionService(eventPublisher, repository, List.of(SQ_DTO_1), List.of(), SonarCloudActiveEnvironment.prod(), null, null); underTest.didUpdateConnections(List.of(SQ_DTO_1_DUP), List.of()); assertThat(repository.getConnectionById("sq1")) .asInstanceOf(InstanceOfAssertFactories.type(SonarQubeConnectionConfiguration.class)) .extracting(SonarQubeConnectionConfiguration::getConnectionId, SonarQubeConnectionConfiguration::getUrl, SonarQubeConnectionConfiguration::isDisableNotifications, SonarQubeConnectionConfiguration::getKind) .containsOnly("sq1", "http://url1_dup", true, ConnectionKind.SONARQUBE); var captor = ArgumentCaptor.forClass(ConnectionConfigurationUpdatedEvent.class); verify(eventPublisher, times(1)).publishEvent(captor.capture()); var events = captor.getAllValues(); assertThat(events).extracting(ConnectionConfigurationUpdatedEvent::updatedConnectionId).containsExactly("sq1"); } @Test void update_connection_should_log_if_unknown_connection_and_add() { var mockedRepo = mock(ConnectionConfigurationRepository.class); underTest = new ConnectionService(eventPublisher, mockedRepo, List.of(), List.of(), SonarCloudActiveEnvironment.prod(), null, null); // Emulate a race condition on the repository: the connection is gone between get and add when(mockedRepo.getConnectionsById()).thenReturn(Map.of(SQ_DTO_2.getConnectionId(), new SonarQubeConnectionConfiguration(SQ_DTO_2.getConnectionId(), "http://foo", true))); when(mockedRepo.addOrReplace(any())).thenReturn(null); underTest.didUpdateConnections(List.of(SQ_DTO_2), List.of()); assertThat(logTester.logs(LogOutput.Level.DEBUG)).containsExactly("Attempt to update connection 'sq2' that was not registered. Possibly a race condition?"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/DtoMapperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.tracking.TrackedIssue; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; class DtoMapperTests { @Test void should_throw_if_hotspot_has_no_vulnerability_probability() { var trackedIssue = mock(TrackedIssue.class); assertThrows(IllegalStateException.class, () -> DtoMapper.toRaisedHotspotDto(trackedIssue, null, true)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarCloudActiveEnvironmentTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.net.URI; import java.util.Map; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarQubeCloudRegionDto; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class SonarCloudActiveEnvironmentTests { private static URI baseUri = URI.create("baseUri"); private static URI apiUri = URI.create("apiUri"); private static URI webSocketUri = URI.create("webSocketUri"); private static SonarQubeCloudRegionDto regionWithBaseUri = new SonarQubeCloudRegionDto(baseUri, null, null); private static SonarQubeCloudRegionDto regionWithApiUri = new SonarQubeCloudRegionDto(null, apiUri, null); private static SonarQubeCloudRegionDto regionWithWebSocketUri = new SonarQubeCloudRegionDto(null, null, webSocketUri); @Test void test_getUri() { assertThat(SonarCloudActiveEnvironment.prod().getUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getProductionUri()); assertThat(SonarCloudActiveEnvironment.prod().getUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getProductionUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithBaseUri)) .getUri(SonarCloudRegion.EU)) .isEqualTo(baseUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithBaseUri)) .getUri(SonarCloudRegion.US)) .isEqualTo(baseUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithApiUri)) .getUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getProductionUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithApiUri)) .getUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getProductionUri()); } @Test void test_getApiUri() { assertThat(SonarCloudActiveEnvironment.prod().getApiUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getApiProductionUri()); assertThat(SonarCloudActiveEnvironment.prod().getApiUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getApiProductionUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithApiUri)) .getApiUri(SonarCloudRegion.EU)) .isEqualTo(apiUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithApiUri)) .getApiUri(SonarCloudRegion.US)) .isEqualTo(apiUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithBaseUri)) .getApiUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getApiProductionUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithBaseUri)) .getApiUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getApiProductionUri()); } @Test void test_getWebSocketsEndpointUri() { assertThat(SonarCloudActiveEnvironment.prod().getWebSocketsEndpointUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getWebSocketUri()); assertThat(SonarCloudActiveEnvironment.prod().getWebSocketsEndpointUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getWebSocketUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithWebSocketUri)). getWebSocketsEndpointUri(SonarCloudRegion.EU)) .isEqualTo(webSocketUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithWebSocketUri)) .getWebSocketsEndpointUri(SonarCloudRegion.US)) .isEqualTo(webSocketUri); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithApiUri)) .getWebSocketsEndpointUri(SonarCloudRegion.EU)) .isEqualTo(SonarCloudRegion.EU.getWebSocketUri()); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithApiUri)) .getWebSocketsEndpointUri(SonarCloudRegion.US)) .isEqualTo(SonarCloudRegion.US.getWebSocketUri()); } @Test void test_isSonarQubeCloud() { assertThat(SonarCloudActiveEnvironment.prod().isSonarQubeCloud("aaaa")).isFalse(); assertThat(SonarCloudActiveEnvironment.prod() .isSonarQubeCloud(SonarCloudRegion.EU.getProductionUri().toString())).isTrue(); assertThat(SonarCloudActiveEnvironment.prod() .isSonarQubeCloud(SonarCloudRegion.US.getProductionUri().toString())).isTrue(); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithBaseUri)) .isSonarQubeCloud(baseUri.toString())).isTrue(); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithApiUri)) .isSonarQubeCloud(SonarCloudRegion.EU.getProductionUri().toString())).isTrue(); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithBaseUri)) .isSonarQubeCloud(baseUri.toString())).isTrue(); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithApiUri)) .isSonarQubeCloud(SonarCloudRegion.US.getProductionUri().toString())).isTrue(); } @Test void test_getRegionOrThrow() { assertThatThrownBy(() -> SonarCloudActiveEnvironment.prod().getRegionOrThrow("aaaa")) .isInstanceOf(IllegalArgumentException.class); assertThat(SonarCloudActiveEnvironment.prod() .getRegionOrThrow(SonarCloudRegion.EU.getProductionUri().toString())).isEqualTo(SonarCloudRegion.EU); assertThat(SonarCloudActiveEnvironment.prod() .getRegionOrThrow(SonarCloudRegion.US.getProductionUri().toString())).isEqualTo(SonarCloudRegion.US); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.EU, regionWithBaseUri)) .getRegionOrThrow(baseUri.toString())).isEqualTo(SonarCloudRegion.EU); assertThat(new SonarCloudActiveEnvironment( Map.of(org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion.US, regionWithBaseUri)) .getRegionOrThrow(baseUri.toString())).isEqualTo(SonarCloudRegion.US); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarProjectsCacheTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.List; import java.util.Optional; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.SonarProjectDto; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SonarProjectsCacheTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); public static final String SQ_1 = "sq1"; public static final String PROJECT_KEY_1 = "projectKey1"; public static final String PROJECT_KEY_2 = "projectKey2"; public static final String PROJECT_NAME_1 = "Project 1"; public static final String PROJECT_NAME_2 = "Project 2"; public static final ServerProject PROJECT_1 = new ServerProject(PROJECT_KEY_1, PROJECT_NAME_1, false); public static final ServerProject PROJECT_1_CHANGED = new ServerProject(PROJECT_KEY_1, PROJECT_NAME_2, false); public static final ServerProject PROJECT_2 = new ServerProject(PROJECT_KEY_2, PROJECT_NAME_2, false); private final ServerApi serverApi = mock(ServerApi.class, Mockito.RETURNS_DEEP_STUBS); private final SonarQubeClientManager sonarQubeClientManager = mock(SonarQubeClientManager.class); private final SonarProjectsCache underTest = new SonarProjectsCache(sonarQubeClientManager); @BeforeEach void setup() { when(sonarQubeClientManager.withActiveClientAndReturn(any(), any())).thenAnswer( invocation -> Optional.ofNullable(((Function) invocation.getArguments()[1]).apply(serverApi))); } @Test void getSonarProject_should_query_server_once() { when(serverApi.component().getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))) .thenReturn(Optional.of(PROJECT_1)) .thenThrow(new AssertionError("Should only be called once")); var sonarProjectCall1 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall1).isPresent(); assertThat(sonarProjectCall1.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall1.get().name()).isEqualTo(PROJECT_NAME_1); var sonarProjectCall2 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall2).isPresent(); assertThat(sonarProjectCall2.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall2.get().name()).isEqualTo(PROJECT_NAME_1); verify(serverApi.component(), times(1)).getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class)); } @Test void getSonarProject_should_cache_failure() { when(serverApi.component().getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))) .thenThrow(new RuntimeException("Unable to fetch project")) .thenReturn(Optional.of(PROJECT_1)); var sonarProjectCall1 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall1).isEmpty(); var sonarProjectCall2 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall2).isEmpty(); verify(serverApi.component(), times(1)).getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class)); } @Test void evict_cache_if_connection_removed_to_save_memory() { when(serverApi.component().getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))) .thenReturn(Optional.of(PROJECT_1)); var sonarProjectCall1 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall1).isPresent(); assertThat(sonarProjectCall1.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall1.get().name()).isEqualTo(PROJECT_NAME_1); underTest.connectionRemoved(new ConnectionConfigurationRemovedEvent(SQ_1)); var sonarProjectCall2 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall2).isPresent(); assertThat(sonarProjectCall2.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall2.get().name()).isEqualTo(PROJECT_NAME_1); verify(serverApi.component(), times(2)).getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class)); } @Test void evict_cache_if_connection_updated_to_refresh_on_next_get() { when(serverApi.component().getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class))) .thenReturn(Optional.of(PROJECT_1)) .thenReturn(Optional.of(PROJECT_1_CHANGED)); var sonarProjectCall1 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall1).isPresent(); assertThat(sonarProjectCall1.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall1.get().name()).isEqualTo(PROJECT_NAME_1); underTest.connectionUpdated(new ConnectionConfigurationUpdatedEvent(SQ_1)); var sonarProjectCall2 = underTest.getSonarProject(SQ_1, PROJECT_KEY_1, new SonarLintCancelMonitor()); assertThat(sonarProjectCall2).isPresent(); assertThat(sonarProjectCall2.get().key()).isEqualTo(PROJECT_KEY_1); assertThat(sonarProjectCall2.get().name()).isEqualTo(PROJECT_NAME_2); verify(serverApi.component(), times(2)).getProject(eq(PROJECT_KEY_1), any(SonarLintCancelMonitor.class)); } @Test void getTextSearchIndex_should_query_server_once() { when(serverApi.component().getAllProjects(any())) .thenReturn(List.of(PROJECT_1, PROJECT_2)) .thenThrow(new AssertionError("Should only be called once")); var searchIndex1 = underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex1.size()).isEqualTo(2); var searchIndex2 = underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex2.size()).isEqualTo(2); verify(serverApi.component(), times(1)).getAllProjects(any()); } @Test void getTextSearchIndex_should_return_empty_index_if_no_projects() { when(serverApi.component().getAllProjects(any())) .thenReturn(List.of()) .thenThrow(new AssertionError("Should only be called once")); var searchIndex1 = underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex1.isEmpty()).isTrue(); underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex1.isEmpty()).isTrue(); verify(serverApi.component(), times(1)).getAllProjects(any()); } @Test void getTextSearchIndex_should_cache_failure() { when(serverApi.component().getAllProjects(any())) .thenThrow(new RuntimeException("Unable to fetch projects")) .thenReturn(List.of(PROJECT_1, PROJECT_2)); var searchIndex1 = underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex1.isEmpty()).isTrue(); underTest.getTextSearchIndex(SQ_1, new SonarLintCancelMonitor()); assertThat(searchIndex1.isEmpty()).isTrue(); verify(serverApi.component(), times(1)).getAllProjects(any()); } @Test void fuzzySearchProjects_should_search_by_both_key_and_name_splitting_by_underscore() { var project1 = new ServerProject("mySearchTerm", "project", false); var project2 = new ServerProject("key", "searchTerm__00", false); var projectNotFound = new ServerProject("SonarSource_peachee-dotnet", "DriAutomation.NET", false); var projectFound = new ServerProject("SonarSource_sonarsource-infra-peach", "sonarsource-infra-peach", false); when(serverApi.component().getAllProjects(any())) .thenReturn(List.of(project1, project2, projectNotFound, projectFound)); var actual = underTest.fuzzySearchProjects(SQ_1, "peach", new SonarLintCancelMonitor()); assertThat(actual).containsExactlyInAnyOrder( new SonarProjectDto("SonarSource_peachee-dotnet", "DriAutomation.NET"), new SonarProjectDto("SonarSource_sonarsource-infra-peach", "sonarsource-infra-peach") ); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/SonarQubeClientManagerTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.net.URI; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetCredentialsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SonarQubeClientManagerTests { private static final String API_SYSTEM_STATUS = "/api/system/status"; @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final ConnectionConfigurationRepository connectionRepository = mock(ConnectionConfigurationRepository.class); private final HttpClientProvider httpClientProvider = mock(HttpClientProvider.class); private SonarLintRpcClient client; private SonarQubeClientManager underTest; @BeforeEach void setUp() { client = mock(SonarLintRpcClient.class); when(client.getCredentials(any())).thenReturn(CompletableFuture.completedFuture(new GetCredentialsResponse(new TokenDto("token")))); underTest = new SonarQubeClientManager(connectionRepository, httpClientProvider, SonarCloudActiveEnvironment.prod(), client); } @Test void getValidClientOrThrow_for_sonarqube() { setupServerConnection("sqs1", "serverUrl"); var connection = underTest.getValidClientOrThrow("sqs1"); assertThat(connection.isActive()).isTrue(); } @Test void getValidClientOrThrow_for_sonarcloud() { setupCloudConnection("sqc1", SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri()); var connection = underTest.getValidClientOrThrow("sqc1"); assertThat(connection.isActive()).isTrue(); } @Test void getValidClientOrThrow_for_sonarcloud_with_trailing_slash_notConnected() { var uriWithSlash = URI.create(SonarCloudRegion.EU.getProductionUri() + "/"); setupCloudConnection("sqc-with-slash", uriWithSlash, SonarCloudRegion.EU.getApiProductionUri()); var connection = underTest.getValidClientOrThrow("sqc-with-slash"); assertThat(connection.isActive()).isTrue(); } @Test void getValidClientOrThrow_should_throw_if_connection_doesnt_exist() { var throwable = catchThrowable(() -> underTest.getValidClientOrThrow("sqc1")); assertThat(throwable.getMessage()).isEqualTo("Connection 'sqc1' is not valid"); } @Test void withActiveClient_should_execute_consumer_when_valid_client_exists() { setupServerConnection("sqs1", "serverUrl"); var consumerExecuted = new AtomicBoolean(false); underTest.withActiveClient("sqs1", api -> consumerExecuted.set(true)); assertThat(consumerExecuted.get()).isTrue(); } @Test void withActiveClient_should_not_execute_consumer_when_connection_not_found() { when(connectionRepository.getConnectionById("nonexistent")).thenReturn(null); var consumerExecuted = new AtomicBoolean(false); underTest.withActiveClient("nonexistent", api -> consumerExecuted.set(true)); assertThat(consumerExecuted.get()).isFalse(); assertThat(logTester.logs()).contains("Connection 'nonexistent' is gone"); } @Test void withActiveClient_should_not_execute_consumer_and_notify_user_when_client_becomes_inactive() { setupServerConnection("sqs1", "serverUrl"); var consumerExecuted = new AtomicBoolean(false); underTest.withActiveClient("sqs1", api -> { throw new UnauthorizedException("401"); }); underTest.withActiveClient("sqs1", api -> consumerExecuted.set(true)); assertThat(consumerExecuted.get()).isFalse(); assertThat(logTester.logs()).contains("Connection 'sqs1' is invalid"); verify(client, times(1)).invalidToken(any()); } @Test void withActiveClient_should_cache_clients_and_reuse_them() { setupServerConnection("sqs1", "serverUrl"); var executionCount = new AtomicInteger(0); underTest.withActiveClient("sqs1", api -> executionCount.incrementAndGet()); underTest.withActiveClient("sqs1", api -> executionCount.incrementAndGet()); underTest.withActiveClient("sqs1", api -> executionCount.incrementAndGet()); assertThat(executionCount.get()).isEqualTo(3); verify(httpClientProvider, times(1)).getHttpClientWithPreemptiveAuth("token", false); } @Test void withActiveClientAndReturn_should_return_value_when_valid_client_exists() { setupServerConnection("sqs1", "serverUrl"); var result = underTest.withActiveClientAndReturn("sqs1", api -> "test-result"); assertThat(result).isPresent().get().isEqualTo("test-result"); } @Test void withActiveClientAndReturn_should_return_empty_when_connection_not_found() { when(connectionRepository.getConnectionById("nonexistent")).thenReturn(null); var result = underTest.withActiveClientAndReturn("nonexistent", api -> "test-result"); assertThat(result).isEmpty(); } @Test void withActiveClientAndReturn_should_return_empty_when_client_inactive() { setupServerConnection("sqs1", "serverUrl"); underTest.withActiveClient("sqs1", api -> { throw new UnauthorizedException("401"); }); var result = underTest.withActiveClientAndReturn("sq1", api -> "test-result"); assertThat(result).isEmpty(); } @Test void withActiveClientFlatMapOptionalAndReturn_should_return_optional_when_valid_client_exists() { setupServerConnection("sqs1", "serverUrl"); var result = underTest.withActiveClientFlatMapOptionalAndReturn("sqs1", api -> Optional.of("test-result")); assertThat(result).isPresent().get().isEqualTo("test-result"); } @Test void withActiveClientFlatMapOptionalAndReturn_should_return_empty_when_function_returns_empty() { setupServerConnection("sqs1", "serverUrl"); var result = underTest.withActiveClientFlatMapOptionalAndReturn("sqs1", api -> Optional.empty()); assertThat(result).isEmpty(); } @Test void withActiveClientFlatMapOptionalAndReturn_should_return_empty_when_connection_not_found() { var result = underTest.withActiveClientFlatMapOptionalAndReturn("nonexistent", api -> Optional.of("test-result")); assertThat(result).isEmpty(); } @Test void withActiveClient_should_not_execute_consumer_when_invalid_credentials() { var httpClient = mock(HttpClient.class); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); setupSuccessfulStatusResponse(httpClient, "serverUrl" + API_SYSTEM_STATUS); when(connectionRepository.getConnectionById("connectionId")) .thenReturn(new SonarQubeConnectionConfiguration("connectionId", "serverUrl", true)); when(client.getCredentials(any())).thenReturn(CompletableFuture.completedFuture(new GetCredentialsResponse(new TokenDto(null)))); var consumerExecuted = new AtomicBoolean(false); underTest.withActiveClient("connectionId", api -> consumerExecuted.set(true)); assertThat(consumerExecuted.get()).isFalse(); verify(client).invalidToken(refEq(new InvalidTokenParams("connectionId"))); } private void setupServerConnection(String connectionId, String serverUrl) { when(connectionRepository.getConnectionById(connectionId)) .thenReturn(new SonarQubeConnectionConfiguration(connectionId, serverUrl, true)); var httpClient = mock(HttpClient.class); when(httpClientProvider.getHttpClientWithPreemptiveAuth("token", false)).thenReturn(httpClient); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); setupSuccessfulStatusResponse(httpClient, serverUrl + API_SYSTEM_STATUS); } private void setupCloudConnection(String connectionId, URI prodUri, URI apiUri) { when(connectionRepository.getConnectionById(connectionId)) .thenReturn(new SonarCloudConnectionConfiguration(prodUri, apiUri, connectionId, "organizationKey", SonarCloudRegion.EU, false)); var httpClient = mock(HttpClient.class); when(httpClientProvider.getHttpClientWithPreemptiveAuth("token", true)).thenReturn(httpClient); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); setupSuccessfulStatusResponse(httpClient, API_SYSTEM_STATUS); } private void setupSuccessfulStatusResponse(HttpClient httpClient, String statusPath) { var httpResponse = mock(HttpClient.Response.class); when(httpResponse.isSuccessful()).thenReturn(true); when(httpResponse.bodyAsString()).thenReturn("{\"id\": \"20160308094653\",\"version\": \"9.9\",\"status\": \"UP\"}"); when(httpClient.getAsyncAnonymous(statusPath)).thenReturn(CompletableFuture.completedFuture(httpResponse)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/TelemetryServerAttributesProviderTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.Map; import java.util.Optional; import java.util.Set; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.StandaloneRuleConfigDto; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.telemetry.TelemetryServerAttributesProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class TelemetryServerAttributesProviderTests { @Test void it_should_calculate_connectedMode_usesSC_notDisabledNotifications_telemetry_attrs() { var configurationScopeId = "scopeId"; var connectionId = "connectionId"; var projectKey = "projectKey"; var configurationRepository = mock(ConfigurationRepository.class); when(configurationRepository.getAllBoundScopes()).thenReturn(Set.of(new BoundScope(configurationScopeId, connectionId, projectKey))); var connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); when(connectionConfigurationRepository.getConnectionById(connectionId)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), connectionId, "myTestOrg", SonarCloudRegion.EU, false)); var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class), mock(StorageService.class)); var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes(); assertThat(telemetryLiveAttributes.usesConnectedMode()).isTrue(); assertThat(telemetryLiveAttributes.usesSonarCloud()).isTrue(); assertThat(telemetryLiveAttributes.childBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeServerBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeCloudEUBindingCount()).isEqualTo(1); assertThat(telemetryLiveAttributes.sonarQubeCloudUSBindingCount()).isZero(); assertThat(telemetryLiveAttributes.devNotificationsDisabled()).isFalse(); assertThat(telemetryLiveAttributes.nonDefaultEnabledRules()).isEmpty(); assertThat(telemetryLiveAttributes.defaultDisabledRules()).isEmpty(); } @Test void it_should_calculate_connectedMode_notUsesSC_disabledDevNotifications_telemetry_attrs() { var configurationScopeId1 = "scopeId_1"; var configurationScopeId2 = "scopeId_2"; var configurationScopeId3 = "scopeId_3"; var connectionId1 = "connectionId_1"; var connectionId2 = "connectionId_2"; var projectKey1 = "projectKey1"; var projectKey2 = "projectKey2"; var configurationRepository = mock(ConfigurationRepository.class); when(configurationRepository.getAllBoundScopes()).thenReturn(Set.of( new BoundScope(configurationScopeId1, connectionId1, projectKey1), new BoundScope(configurationScopeId2, connectionId2, projectKey2))); when(configurationRepository.getLeafConfigScopeIds()).thenReturn(Set.of(configurationScopeId2, configurationScopeId3)); when(configurationRepository.getConfigurationScope(configurationScopeId1)).thenReturn(new ConfigurationScope(configurationScopeId1, null, false, "1")); when(configurationRepository.getConfigurationScope(configurationScopeId2)).thenReturn(new ConfigurationScope(configurationScopeId2, configurationScopeId1, false, "2")); when(configurationRepository.getConfigurationScope(configurationScopeId3)).thenReturn(new ConfigurationScope(configurationScopeId3, configurationScopeId1, false, "3")); when(configurationRepository.getBindingConfiguration(configurationScopeId1)).thenReturn(new BindingConfiguration(configurationScopeId1, projectKey1, false)); when(configurationRepository.getBindingConfiguration(configurationScopeId2)).thenReturn(new BindingConfiguration(configurationScopeId2, projectKey2, false)); when(configurationRepository.getBindingConfiguration(configurationScopeId3)).thenReturn(new BindingConfiguration(null, null, false)); var connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); when(connectionConfigurationRepository.getConnectionById(connectionId1)).thenReturn(new SonarQubeConnectionConfiguration(connectionId1, "www.squrl1.org", false)); when(connectionConfigurationRepository.getConnectionById(connectionId2)).thenReturn(new SonarQubeConnectionConfiguration(connectionId2, "www.squrl2.org", true)); var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class), mock(StorageService.class)); var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes(); assertThat(telemetryLiveAttributes.usesConnectedMode()).isTrue(); assertThat(telemetryLiveAttributes.usesSonarCloud()).isFalse(); assertThat(telemetryLiveAttributes.childBindingCount()).isEqualTo(1); assertThat(telemetryLiveAttributes.sonarQubeServerBindingCount()).isEqualTo(2); assertThat(telemetryLiveAttributes.sonarQubeCloudEUBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeCloudUSBindingCount()).isZero(); assertThat(telemetryLiveAttributes.devNotificationsDisabled()).isTrue(); assertThat(telemetryLiveAttributes.nonDefaultEnabledRules()).isEmpty(); assertThat(telemetryLiveAttributes.defaultDisabledRules()).isEmpty(); } @Test void it_should_calculate_disabledRules_enabledRules_telemetry_attrs() { var activeRulesService = mock(ActiveRulesService.class); when(activeRulesService.getStandaloneRuleConfig()).thenReturn( Map.of("ruleKey_1", new StandaloneRuleConfigDto(true, Map.of()), "ruleKey_2", new StandaloneRuleConfigDto(true, Map.of()), "ruleKey_3", new StandaloneRuleConfigDto(false, Map.of()), "ruleKey_4", new StandaloneRuleConfigDto(false, Map.of()))); var rulesRepository = mock(RulesRepository.class); var sonarLintRuleDefinition1 = getSonarLintRuleDefinition(true); var sonarLintRuleDefinition2 = getSonarLintRuleDefinition(false); var sonarLintRuleDefinition3 = getSonarLintRuleDefinition(true); var sonarLintRuleDefinition4 = getSonarLintRuleDefinition(false); when(rulesRepository.getEmbeddedRule("ruleKey_1")).thenReturn(sonarLintRuleDefinition1); when(rulesRepository.getEmbeddedRule("ruleKey_2")).thenReturn(sonarLintRuleDefinition2); when(rulesRepository.getEmbeddedRule("ruleKey_3")).thenReturn(sonarLintRuleDefinition3); when(rulesRepository.getEmbeddedRule("ruleKey_4")).thenReturn(sonarLintRuleDefinition4); var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), activeRulesService, rulesRepository, mock(NodeJsService.class), mock(StorageService.class)); var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes(); assertThat(telemetryLiveAttributes.nonDefaultEnabledRules()).containsExactly("ruleKey_2"); assertThat(telemetryLiveAttributes.defaultDisabledRules()).containsExactly("ruleKey_3"); assertThat(telemetryLiveAttributes.usesConnectedMode()).isFalse(); assertThat(telemetryLiveAttributes.childBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeServerBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeCloudEUBindingCount()).isZero(); assertThat(telemetryLiveAttributes.sonarQubeCloudUSBindingCount()).isZero(); assertThat(telemetryLiveAttributes.usesSonarCloud()).isFalse(); assertThat(telemetryLiveAttributes.devNotificationsDisabled()).isFalse(); } @Test void it_should_test_nodejs_version_telemetry_attr() { var nodeJsService = mock(NodeJsService.class); var version = "3.1.4.159"; when(nodeJsService.getActiveNodeJsVersion()).thenReturn(Optional.of(Version.create(version))); var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), mock(ActiveRulesService.class), mock(RulesRepository.class), nodeJsService, mock(StorageService.class)); assertThat(underTest.getTelemetryServerLiveAttributes().nodeVersion()).isEqualTo(version); } @NotNull private static Optional getSonarLintRuleDefinition(boolean isActiveByDefault) { var sonarLintRuleDefinition = mock(SonarLintRuleDefinition.class); when(sonarLintRuleDefinition.isActiveByDefault()).thenReturn(isActiveByDefault); return Optional.of(sonarLintRuleDefinition); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/UserPathsTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class UserPathsTests { @Test void default_home_should_be_in_user_home() { assertThat(UserPaths.computeUserHome(null)).isEqualTo(Paths.get(System.getProperty("user.home")).resolve(".sonarlint")); } @Test void env_setting_should_override_default_home() { assertThat(UserPaths.computeUserHome("clientPath")).isEqualTo(Paths.get("clientPath")); } @Test void should_return_telemetry_home() { var initializeParams = mock(InitializeParams.class); when(initializeParams.getSonarlintUserHome()).thenReturn("~/.sonarlint"); when(initializeParams.getTelemetryConstantAttributes()).thenReturn( new TelemetryClientConstantAttributesDto("eclipse", "---", "1.2.3", "4.5.6", null)); var userPaths = UserPaths.from(initializeParams); var telemetryDir = userPaths.getHomeIdeSpecificDir("telemetry"); assertThat(telemetryDir) .isEqualTo(Path.of("~/.sonarlint/telemetry/eclipse")); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/VersionSoonUnsupportedHelperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScopeWithBinding; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverconnection.VersionUtils; import org.sonarsource.sonarlint.core.sync.SynchronizationService; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @Disabled("SLCORE-685 Some tests fail depending on the current date") class VersionSoonUnsupportedHelperTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String CONFIG_SCOPE_ID = "configScopeId"; private static final String CONFIG_SCOPE_ID_2 = "configScopeId2"; private static final String SQ_CONNECTION_ID = "sqConnectionId"; private static final String SQ_CONNECTION_ID_2 = "sqConnectionId2"; private static final String SC_CONNECTION_ID = "scConnectionId"; private static final SonarQubeConnectionConfiguration SQ_CONNECTION = new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID, "https://mysonarqube.com", true); private static final SonarQubeConnectionConfiguration SQ_CONNECTION_2 = new SonarQubeConnectionConfiguration(SQ_CONNECTION_ID_2, "https://mysonarqube2.com", true); private static final SonarCloudConnectionConfiguration SC_CONNECTION = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), SC_CONNECTION_ID, "https://sonarcloud.com", SonarCloudRegion.EU, true); private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); private final SynchronizationService synchronizationService = mock(SynchronizationService.class); private ConfigurationRepository configRepository; private ConnectionConfigurationRepository connectionRepository; private VersionSoonUnsupportedHelper underTest; @BeforeEach void init() { configRepository = new ConfigurationRepository(); connectionRepository = new ConnectionConfigurationRepository(); underTest = new VersionSoonUnsupportedHelper(client, configRepository, mock(SonarQubeClientManager.class), connectionRepository, synchronizationService); } @Test void should_trigger_notification_when_new_binding_to_previous_lts_detected_on_config_scope_event() { var bindingConfiguration = new BindingConfiguration(SQ_CONNECTION_ID, "", true); configRepository.addOrReplace(new ConfigurationScope(CONFIG_SCOPE_ID, null, false, ""), bindingConfiguration); configRepository.addOrReplace(new ConfigurationScope(CONFIG_SCOPE_ID_2, null, false, ""), bindingConfiguration); connectionRepository.addOrReplace(SQ_CONNECTION); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), any(), any(SonarLintCancelMonitor.class))) .thenReturn(VersionUtils.getMinimalSupportedVersion()); underTest.configurationScopesAdded(new ConfigurationScopesAddedWithBindingEvent(Set.of( new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "scope1"), BindingConfiguration.noBinding()), new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID_2, null, true, "scope2"), BindingConfiguration.noBinding())))); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsOnly("Connection '" + SQ_CONNECTION_ID + "' with version '" + VersionUtils.getMinimalSupportedVersion().getName() + "' is detected to be soon unsupported")); } @Test void should_trigger_multiple_notification_when_new_bindings_to_previous_lts_detected_on_config_scope_event() { var bindingConfiguration = new BindingConfiguration(SQ_CONNECTION_ID, "", true); var bindingConfiguration2 = new BindingConfiguration(SQ_CONNECTION_ID_2, "", true); configRepository.addOrReplace(new ConfigurationScope(CONFIG_SCOPE_ID, null, false, ""), bindingConfiguration); configRepository.addOrReplace(new ConfigurationScope(CONFIG_SCOPE_ID_2, null, false, ""), bindingConfiguration2); connectionRepository.addOrReplace(SQ_CONNECTION); connectionRepository.addOrReplace(SQ_CONNECTION_2); var serverApi = mock(ServerApi.class); var serverApi2 = mock(ServerApi.class); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))) .thenReturn(VersionUtils.getMinimalSupportedVersion()); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID_2), eq(serverApi2), any(SonarLintCancelMonitor.class))) .thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion() + ".9")); underTest.configurationScopesAdded(new ConfigurationScopesAddedWithBindingEvent(Set.of( new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "scope1"), BindingConfiguration.noBinding()), new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID_2, null, true, "scope2"), BindingConfiguration.noBinding())))); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsOnly( "Connection '" + SQ_CONNECTION_ID + "' with version '" + VersionUtils.getMinimalSupportedVersion().getName() + "' is detected to be soon unsupported", "Connection '" + SQ_CONNECTION_ID_2 + "' with version '" + VersionUtils.getMinimalSupportedVersion() + ".9' is detected to be soon unsupported")); } @Test void should_not_trigger_notification_when_config_scope_has_no_effective_binding() { underTest.configurationScopesAdded(new ConfigurationScopesAddedWithBindingEvent(Set.of( new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "scope1"), BindingConfiguration.noBinding())))); assertThat(logTester.logs()).isEmpty(); } @Test void should_trigger_notification_when_new_binding_to_previous_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))) .thenReturn(VersionUtils.getMinimalSupportedVersion()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsOnly("Connection '" + SQ_CONNECTION_ID + "' with version '" + VersionUtils.getMinimalSupportedVersion().getName() + "' is detected to be soon unsupported")); } @Test void should_trigger_once_when_same_binding_to_previous_lts_detected_twice() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))) .thenReturn(VersionUtils.getMinimalSupportedVersion()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsOnly("Connection '" + SQ_CONNECTION_ID + "' with version '" + VersionUtils.getMinimalSupportedVersion().getName() + "' is detected to be soon unsupported")); } @Test void should_trigger_notification_when_new_binding_to_in_between_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))) .thenReturn(Version.create(VersionUtils.getMinimalSupportedVersion().getName() + ".9")); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); await().untilAsserted(() -> assertThat(logTester.logs(LogOutput.Level.DEBUG)) .containsOnly("Connection '" + SQ_CONNECTION_ID + "' with version '" + VersionUtils.getMinimalSupportedVersion().getName() + ".9' is detected to be soon unsupported")); } @Test void should_not_trigger_notification_when_new_binding_to_current_lts_detected() { connectionRepository.addOrReplace(SQ_CONNECTION); var serverApi = mock(ServerApi.class); when(synchronizationService.readOrSynchronizeServerVersion(eq(SQ_CONNECTION_ID), eq(serverApi), any(SonarLintCancelMonitor.class))).thenReturn(VersionUtils.getCurrentLts()); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SQ_CONNECTION_ID, "", false))); assertThat(logTester.logs()).isEmpty(); } @Test void should_not_trigger_notification_when_sonarcloud_binding_detected() { connectionRepository.addOrReplace(SC_CONNECTION); underTest.bindingConfigChanged(new BindingConfigChangedEvent(CONFIG_SCOPE_ID, null, new BindingConfiguration(SC_CONNECTION_ID, "", false))); assertThat(logTester.logs()).isEmpty(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/ai/ide/AiHookServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class AiHookServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_generate_nodejs_script_when_nodejs_detected() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.NODEJS)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); var response = service.getHookScriptContent(AiAgent.WINDSURF); assertThat(response.getScriptFileName()).isEqualTo("sonarqube_analysis_hook.js"); assertThat(response.getScriptContent()) .contains("#!/usr/bin/env node") .contains("hostname: 'localhost'") .contains("STARTING_PORT = 64120") .contains("ENDING_PORT = 64130") .contains("path: '/sonarlint/api/analysis/files'") .contains("path: '/sonarlint/api/status'"); assertThat(response.getConfigFileName()).isEqualTo("hooks.json"); assertThat(response.getConfigContent()) .contains("\"post_write_code\"") .contains("{{SCRIPT_PATH}}") .contains("\"show_output\": true"); verify(telemetryService).aiHookInstalled(AiAgent.WINDSURF); } @Test void it_should_generate_python_script_when_python_detected() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64121); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.PYTHON)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); var response = service.getHookScriptContent(AiAgent.WINDSURF); assertThat(response.getScriptFileName()).isEqualTo("sonarqube_analysis_hook.py"); assertThat(response.getScriptContent()) .contains("#!/usr/bin/env python3") .contains("STARTING_PORT = 64120") .contains("ENDING_PORT = 64130") .contains("/sonarlint/api/analysis/files") .contains("/sonarlint/api/status"); assertThat(response.getConfigFileName()).isEqualTo("hooks.json"); assertThat(response.getConfigContent()) .contains("\"post_write_code\"") .contains("{{SCRIPT_PATH}}") .contains("\"show_output\": true"); verify(telemetryService).aiHookInstalled(AiAgent.WINDSURF); } @Test void it_should_generate_bash_script_when_bash_detected() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64122); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.BASH)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); var response = service.getHookScriptContent(AiAgent.WINDSURF); assertThat(response.getScriptFileName()).isEqualTo("sonarqube_analysis_hook.sh"); assertThat(response.getScriptContent()) .contains("#!/bin/bash") .contains("STARTING_PORT=64120") .contains("ENDING_PORT=64130") .contains("/sonarlint/api/analysis/files") .contains("/sonarlint/api/status"); assertThat(response.getConfigFileName()).isEqualTo("hooks.json"); assertThat(response.getConfigContent()) .contains("\"post_write_code\"") .contains("{{SCRIPT_PATH}}") .contains("\"show_output\": true"); verify(telemetryService).aiHookInstalled(AiAgent.WINDSURF); } @Test void it_should_throw_exception_when_embedded_server_not_started() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(-1); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); assertThatThrownBy(() -> service.getHookScriptContent(AiAgent.WINDSURF)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Embedded server is not started"); } @Test void it_should_throw_exception_when_no_executable_found() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.empty()); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); assertThatThrownBy(() -> service.getHookScriptContent(AiAgent.WINDSURF)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("No suitable executable found"); } @Test void it_should_embed_correct_agent_in_script_comment_for_windsurf() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.NODEJS)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); var response = service.getHookScriptContent(AiAgent.WINDSURF); assertThat(response.getScriptContent()) .contains("SonarQube for IDE Windsurf Hook") .contains("EXPECTED_IDE_NAME = 'Windsurf'"); verify(telemetryService).aiHookInstalled(AiAgent.WINDSURF); } @Test void it_should_throw_exception_for_unsupported_cursor_agent() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.PYTHON)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); assertThatThrownBy(() -> service.getHookScriptContent(AiAgent.CURSOR)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("hook configuration not yet implemented"); } @Test void it_should_throw_exception_for_unsupported_kiro_agent() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.PYTHON)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); assertThatThrownBy(() -> service.getHookScriptContent(AiAgent.KIRO)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("hook configuration not yet implemented"); } @Test void it_should_throw_exception_for_unsupported_github_copilot_agent() { var embeddedServer = mock(EmbeddedServer.class); var telemetryService = mock(TelemetryService.class); var executableLocator = mock(ExecutableLocator.class); when(embeddedServer.getPort()).thenReturn(64120); when(executableLocator.detectBestExecutable()).thenReturn(Optional.of(HookScriptType.BASH)); var service = new AiHookService(embeddedServer, telemetryService, executableLocator); assertThatThrownBy(() -> service.getHookScriptContent(AiAgent.GITHUB_COPILOT)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("GitHub Copilot does not support hooks"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/ai/ide/ExecutableLocatorTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.ai.ide; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandExecutor; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.nodejs.InstalledNodeJs; import org.sonarsource.sonarlint.core.nodejs.NodeJsHelper; import org.sonar.api.utils.command.StreamConsumer; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ExecutableLocatorTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_find_nodejs_when_available() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(new InstalledNodeJs(Paths.get("/usr/bin/node"), Version.create("18.0.0"))); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var result = locator.detectBestExecutable(); assertThat(result) .isPresent() .contains(HookScriptType.NODEJS); } @Test void it_should_find_python_when_nodejs_not_available() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(false); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())).thenReturn(0); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { if (command.toCommandLine().contains("python3")) { return "/usr/bin/python3"; } return null; } }; var result = locator.detectBestExecutable(); assertThat(result) .isPresent() .contains(HookScriptType.PYTHON); } @Test @EnabledOnOs(value = OS.LINUX) void it_should_fallback_to_bash_on_unix_when_no_nodejs_or_python() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(false); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { return null; // Python not found } }; var result = locator.detectBestExecutable(); assertThat(result) .isPresent() .contains(HookScriptType.BASH); } @Test @EnabledOnOs(value = OS.WINDOWS) void it_should_return_empty_on_windows_when_only_bash_available() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(true); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { return null; // Neither Python nor Bash found on Windows } }; var result = locator.detectBestExecutable(); assertThat(result).isEmpty(); } @Test void it_should_cache_detection_result() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(new InstalledNodeJs(Paths.get("/usr/bin/node"), Version.create("18.0.0"))); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); // Call twice var result1 = locator.detectBestExecutable(); var result2 = locator.detectBestExecutable(); assertThat(result1).isPresent(); assertThat(result2).isPresent(); assertThat(result1).contains(HookScriptType.NODEJS); assertThat(result2).contains(HookScriptType.NODEJS); // Verify autoDetect was only called once due to caching verify(nodeJsHelper, times(1)).autoDetect(); } @Test void it_should_fallback_to_python_when_python3_not_found() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(false); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { if (command.toCommandLine().contains("python3")) { return null; // python3 not found } if (command.toCommandLine().contains("python")) { return "/usr/bin/python"; } return null; } }; var result = locator.detectBestExecutable(); assertThat(result) .isPresent() .contains(HookScriptType.PYTHON); } @Test @EnabledOnOs(value = OS.WINDOWS) void it_should_detect_bash_on_windows_when_available() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(true); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { if (command.toCommandLine().contains("bash.exe")) { return "C:\\Program Files\\Git\\bin\\bash.exe"; } return null; } }; var result = locator.detectBestExecutable(); assertThat(result) .isPresent() .contains(HookScriptType.BASH); } @Test void it_should_handle_nodejs_detection_error_gracefully() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenThrow(new RuntimeException("Node.js detection failed")); when(system2.isOsWindows()).thenReturn(false); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { if (command.toCommandLine().contains("python3")) { return "/usr/bin/python3"; } return null; } }; var result = locator.detectBestExecutable(); // Should fall back to Python assertThat(result) .isPresent() .contains(HookScriptType.PYTHON); } @Test void runSimpleCommand_should_return_first_line_of_stdout_on_success() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())).thenAnswer(invocation -> { var stdOutConsumer = (StreamConsumer) invocation.getArgument(1); stdOutConsumer.consumeLine("/usr/bin/python3"); stdOutConsumer.consumeLine("additional output"); return 0; }); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var command = Command.create("which").addArgument("python3"); var result = locator.runSimpleCommand(command); assertThat(result).isEqualTo("/usr/bin/python3"); } @Test void runSimpleCommand_should_return_null_on_non_zero_exit_code() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())).thenAnswer(invocation -> { var stdOutConsumer = (StreamConsumer) invocation.getArgument(1); stdOutConsumer.consumeLine("some output"); return 1; // Non-zero exit code }); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var command = Command.create("which").addArgument("nonexistent"); var result = locator.runSimpleCommand(command); assertThat(result).isNull(); } @Test void runSimpleCommand_should_return_null_on_empty_stdout() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())).thenReturn(0); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var command = Command.create("echo").addArgument(""); var result = locator.runSimpleCommand(command); assertThat(result).isNull(); } @Test void runSimpleCommand_should_return_null_on_command_exception() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())) .thenThrow(new org.sonar.api.utils.command.CommandException(Command.create("test"), "Command failed", null)); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var command = Command.create("invalid_command"); var result = locator.runSimpleCommand(command); assertThat(result).isNull(); } @Test void runSimpleCommand_should_log_stdout_and_stderr() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(commandExecutor.execute(any(Command.class), any(), any(), anyLong())).thenAnswer(invocation -> { var stdOutConsumer = (StreamConsumer) invocation.getArgument(1); var stdErrConsumer = (StreamConsumer) invocation.getArgument(2); stdOutConsumer.consumeLine("/usr/bin/test"); stdErrConsumer.consumeLine("warning message"); return 0; }); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var command = Command.create("test_command"); var result = locator.runSimpleCommand(command); assertThat(result).isEqualTo("/usr/bin/test"); assertThat(logTester.logs()).anyMatch(log -> log.contains("stdout: /usr/bin/test")); assertThat(logTester.logs()).anyMatch(log -> log.contains("stderr: warning message")); } @Test void it_should_return_empty_when_no_executable_found() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/usr/libexec/path_helper"); when(nodeJsHelper.autoDetect()).thenReturn(null); when(system2.isOsWindows()).thenReturn(true); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper) { @Override String runSimpleCommand(@NotNull Command command) { // Simulate no executable found for any command return null; } }; var result = locator.detectBestExecutable(); assertThat(result).isEmpty(); assertThat(logTester.logs()).anyMatch(log -> log.contains("No suitable executable found") || log.contains("not found") || log.contains("not available")); } @Test void it_should_not_set_path_env_when_path_helper_does_not_exist() { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = Paths.get("/nonexistent/path_helper"); when(system2.isOsMac()).thenReturn(true); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var testCommand = Command.create("test"); var commandLineBefore = testCommand.toCommandLine(); locator.computePathEnvForMacOs(testCommand); var commandLineAfter = testCommand.toCommandLine(); // PATH should not be set assertThat(commandLineAfter).isEqualTo(commandLineBefore); } @Test void it_should_not_set_path_env_when_not_on_macos(@TempDir File tempDir) { var system2 = mock(System2.class); var commandExecutor = mock(CommandExecutor.class); var nodeJsHelper = mock(NodeJsHelper.class); var pathHelper = new File(tempDir, "path_helper").toPath(); when(system2.isOsMac()).thenReturn(false); try (var filesMock = mockStatic(Files.class)) { filesMock.when(() -> Files.exists(pathHelper)).thenReturn(true); var locator = new ExecutableLocator(system2, pathHelper, commandExecutor, nodeJsHelper); var testCommand = Command.create("test"); var commandLineBefore = testCommand.toCommandLine(); locator.computePathEnvForMacOs(testCommand); var commandLineAfter = testCommand.toCommandLine(); // PATH should not be set when not on macOS assertThat(commandLineAfter).isEqualTo(commandLineBefore); } } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/AnalysisSchedulerCacheTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.plugin.PluginLifecycleService; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class AnalysisSchedulerCacheTest { @Test void reloadStandalonePlugins_evicts_caches_when_no_scheduler_started(@TempDir Path tempDir) { var userPaths = mockUserPaths(tempDir); var pluginLifecycleService = mock(PluginLifecycleService.class); var cache = new AnalysisSchedulerCache(userPaths, mock(ConfigurationRepository.class), mock(NodeJsService.class), mock(PluginsService.class), pluginLifecycleService, mock(ClientFileSystemService.class)); cache.reloadStandalonePlugins(); verify(pluginLifecycleService).unloadEmbeddedPluginsAndEvictCaches(); } @Test void reloadPlugins_evicts_caches_when_no_connected_scheduler_exists(@TempDir Path tempDir) { var userPaths = mockUserPaths(tempDir); var pluginLifecycleService = mock(PluginLifecycleService.class); var cache = new AnalysisSchedulerCache(userPaths, mock(ConfigurationRepository.class), mock(NodeJsService.class), mock(PluginsService.class), pluginLifecycleService, mock(ClientFileSystemService.class)); cache.reloadPlugins("conn1"); verify(pluginLifecycleService).unloadPluginsAndEvictCaches("conn1"); } private static UserPaths mockUserPaths(Path tempDir) { var userPaths = mock(UserPaths.class); when(userPaths.getWorkDir()).thenReturn(tempDir.resolve("work")); return userPaths; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/BackendInputFileTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.io.File; import java.net.URI; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.fs.ClientFile; import static org.assertj.core.api.Assertions.assertThat; class BackendInputFileTests { @Test void ascii_path_should_be_the_same() { var path = Path.of("/test/file.php"); var pathAsString = path.toString().replace(File.separatorChar, '/'); var clientFile = new ClientFile(URI.create("file://" + pathAsString), "configScopeId", path, false, null, null, null, true); var inputFile = new BackendInputFile(clientFile); assertThat(inputFile.getPath().replace(File.separatorChar, '/')).endsWith(pathAsString); } @Test void non_ascii_path_should_be_the_same() { var path = Path.of("/中文字符/file.php"); var pathAsString = path.toString().replace(File.separatorChar, '/'); var clientFile = new ClientFile(URI.create("file://" + pathAsString), "configScopeId", path, false, null, null, null, true); var inputFile = new BackendInputFile(clientFile); assertThat(inputFile.getPath().replace(File.separatorChar, '/')).endsWith(pathAsString); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/ClientAnalysisPropertiesServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.analysis; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class ClientAnalysisPropertiesServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String CONFIG_SCOPE_ID = "scope-id"; private static final String ANOTHER_CONFIG_SCOPE_ID = "another-scope-id"; UserAnalysisPropertiesRepository underTest; @BeforeEach void setup() { underTest = new UserAnalysisPropertiesRepository(); } @Test void it_should_remove_previous_config_and_set_provided_user_properties() { var properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).isEmpty(); underTest.setUserProperties(CONFIG_SCOPE_ID, Map.of("key1", "value1", "key2", "value2")); properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).hasSize(2).containsEntry("key1", "value1").containsEntry("key2", "value2"); underTest.setUserProperties(CONFIG_SCOPE_ID, Map.of("key2", "new-value2", "key3", "new-value3")); properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).hasSize(2).containsEntry("key2", "new-value2").containsEntry("key3", "new-value3"); } @Test void it_should_not_modify_other_config_scope_properties() { var properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).isEmpty(); underTest.setUserProperties(CONFIG_SCOPE_ID, Map.of("key1", "value1", "key2", "value2")); underTest.setUserProperties(ANOTHER_CONFIG_SCOPE_ID, Map.of("key1", "value1")); properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).hasSize(2).containsEntry("key1", "value1").containsEntry("key2", "value2"); underTest.setUserProperties(CONFIG_SCOPE_ID, Map.of("key2", "new-value2", "key3", "new-value3")); properties = underTest.getUserProperties(CONFIG_SCOPE_ID); assertThat(properties).hasSize(2).containsEntry("key2", "new-value2").containsEntry("key3", "new-value3"); var anotherProperties = underTest.getUserProperties(ANOTHER_CONFIG_SCOPE_ID); assertThat(anotherProperties).hasSize(1).containsEntry("key1", "value1"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/branch/SonarProjectBranchTrackingServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.branch; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.event.ConfigurationScopesAddedWithBindingEvent; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScopeWithBinding; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchSonarProjectBranchResponse; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranches; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranchesStorage; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SonarProjectBranchTrackingServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); public static final String CONNECTION_ID = "connectionId"; public static final String PROJECT_KEY = "projectKey"; public static final String CONFIG_SCOPE_ID = "configScopeId"; private SonarProjectBranchTrackingService underTest; private final SonarLintRpcClient sonarLintRpcClient = mock(SonarLintRpcClient.class); private final StorageService storageService = mock(StorageService.class); private final ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); private ProjectBranchesStorage projectBranchesStorage; @BeforeEach void prepare() { when(configurationRepository.getConfigurationScope(CONFIG_SCOPE_ID)).thenReturn(new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "Test config scope")); var binding = new Binding(CONNECTION_ID, PROJECT_KEY); when(configurationRepository.getEffectiveBinding(CONFIG_SCOPE_ID)).thenReturn(Optional.of(binding)); var sonarProjectStorage = mock(SonarProjectStorage.class); when(storageService.binding(binding)).thenReturn(sonarProjectStorage); projectBranchesStorage = mock(ProjectBranchesStorage.class); when(sonarProjectStorage.branches()).thenReturn(projectBranchesStorage); underTest = new SonarProjectBranchTrackingService(sonarLintRpcClient, storageService, configurationRepository, mock(ApplicationEventPublisher.class)); } @AfterEach void shutdown() { underTest.shutdown(); } @Test void shouldCancelPreviousJobIfNewOneIsSubmitted() { when(projectBranchesStorage.exists()).thenReturn(true); when(projectBranchesStorage.read()).thenReturn(new ProjectBranches(Set.of("main", "feature"), "main")); var firstFuture = new CompletableFuture(); when(sonarLintRpcClient.matchSonarProjectBranch(any())) // Emulate a long response for the first request .thenReturn(firstFuture) .thenReturn(CompletableFuture.completedFuture(new MatchSonarProjectBranchResponse("feature"))); // This should queue a first branch matching underTest.onConfigurationScopesAdded(new ConfigurationScopesAddedWithBindingEvent(Set.of( new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "scope"), BindingConfiguration.noBinding() )))); // Wait for the RPC client to be called verify(sonarLintRpcClient, timeout(1000)).matchSonarProjectBranch(any()); // This should cancel the previous branch matching, and queue a new one underTest.didVcsRepositoryChange(CONFIG_SCOPE_ID); assertThat(underTest.awaitEffectiveSonarProjectBranch(CONFIG_SCOPE_ID)).contains("feature"); assertThat(firstFuture).isCancelled(); verify(sonarLintRpcClient, timeout(1000).times(1)).didChangeMatchedSonarProjectBranch(any()); } @Test void shouldUnlockThoseAwaitingForBranchOnErrorAndDefaultToMain() { when(projectBranchesStorage.exists()).thenReturn(true); when(projectBranchesStorage.read()).thenReturn(new ProjectBranches(Set.of("main", "feature"), "main")); var rpcFuture = new CompletableFuture(); when(sonarLintRpcClient.matchSonarProjectBranch(any())) .thenReturn(rpcFuture); // This should queue a first branch matching underTest.onConfigurationScopesAdded(new ConfigurationScopesAddedWithBindingEvent(Set.of( new ConfigurationScopeWithBinding( new ConfigurationScope(CONFIG_SCOPE_ID, null, true, "scope"), BindingConfiguration.noBinding() )))); // Wait for the RPC client to be called verify(sonarLintRpcClient, timeout(1000)).matchSonarProjectBranch(any()); rpcFuture.completeExceptionally(new RuntimeException("Unexpected error")); assertThat(underTest.awaitEffectiveSonarProjectBranch(CONFIG_SCOPE_ID)).contains("main"); await().untilAsserted(() -> assertThat(logTester.logs()) .contains("Matched Sonar project branch for configuration scope 'configScopeId' changed from 'null' to 'main'")); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/commons/SmartCancelableLoadingCacheTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.commons; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import javax.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.stubbing.Answer; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import testutils.TakeThreadDumpAfter; import testutils.ThreadDumpExtension; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @ExtendWith(ThreadDumpExtension.class) class SmartCancelableLoadingCacheTests { public static final String ANOTHER_VALUE = "anotherValue"; @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); public static final String A_VALUE = "aValue"; public static final String A_KEY = "aKey"; public static final String ANOTHER_KEY = "anotherKey"; private final SmartCancelableLoadingCache.Listener listener = mock(SmartCancelableLoadingCache.Listener.class); private final BiFunction computer = mock(BiFunction.class); private final SmartCancelableLoadingCache underTest = new SmartCancelableLoadingCache<>("test", computer, listener); @AfterEach void close() { underTest.close(); } @Test @TakeThreadDumpAfter(seconds = 10) void should_cache_value_and_notify_listener_once() { when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))).thenReturn(A_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); verify(listener).afterCachedValueRefreshed(A_KEY, null, A_VALUE); verify(computer, times(1)).apply(eq(A_KEY), any()); } @Test @TakeThreadDumpAfter(seconds = 10) void should_wait_for_long_computation() { when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))).thenAnswer(invocation -> { Thread.sleep(100); return A_VALUE; }); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); } @Test @TakeThreadDumpAfter(seconds = 10) void should_throw_if_failure_while_loading() { when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))).thenThrow(new RuntimeException("boom")); assertThrows(RuntimeException.class, () -> underTest.get(A_KEY)); assertThrows(RuntimeException.class, () -> underTest.get(A_KEY)); assertThrows(RuntimeException.class, () -> underTest.get(A_KEY)); verify(computer, times(1)).apply(eq(A_KEY), any()); verifyNoInteractions(listener); } @Test @TakeThreadDumpAfter(seconds = 10) void should_refresh_value() { when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenReturn(A_VALUE) .thenReturn(ANOTHER_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); verify(listener).afterCachedValueRefreshed(A_KEY, null, A_VALUE); underTest.refreshAsync(A_KEY); verify(listener, timeout(1000)).afterCachedValueRefreshed(A_KEY, A_VALUE, ANOTHER_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(ANOTHER_VALUE); verify(computer, times(2)).apply(eq(A_KEY), any()); } @Test @TakeThreadDumpAfter(seconds = 10) void should_cancel_previous_computation_on_refresh() throws InterruptedException { var firstComputationStarted = new CountDownLatch(1); var cancelled = new AtomicBoolean(); when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(firstComputationStarted, cancelled)) .thenReturn(ANOTHER_VALUE); // Queue a first computation underTest.refreshAsync(A_KEY); firstComputationStarted.await(); // Queue a second computation underTest.refreshAsync(A_KEY); assertThat(underTest.get(A_KEY)).isEqualTo(ANOTHER_VALUE); assertThat(cancelled.get()).isTrue(); verify(computer, times(2)).apply(eq(A_KEY), any()); } @Test @TakeThreadDumpAfter(seconds = 10) void should_cancel_previous_computation_on_clear() throws InterruptedException { var firstComputationStarted = new CountDownLatch(1); var cancelled = new AtomicBoolean(); when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(firstComputationStarted, cancelled)); // Queue a first computation underTest.refreshAsync(A_KEY); firstComputationStarted.await(); underTest.clear(A_KEY); await().untilAsserted(() -> assertThat(cancelled.get()).isTrue()); verify(computer, times(1)).apply(eq(A_KEY), any()); } @Test @TakeThreadDumpAfter(seconds = 10) void should_cancel_all_previous_computation_on_close() throws InterruptedException { var key1ComputationStarted = new CountDownLatch(1); var cancelledKey1 = new AtomicBoolean(); when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(key1ComputationStarted, cancelledKey1)); // Queue a computation of key1 underTest.refreshAsync(A_KEY); key1ComputationStarted.await(); var key2ComputationStarted = new CountDownLatch(1); var cancelledKey2 = new AtomicBoolean(); when(computer.apply(eq(ANOTHER_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(key2ComputationStarted, cancelledKey2)); // Queue a computation of key2, that will only start after computation of key1 because the executor service is single threaded underTest.refreshAsync(ANOTHER_KEY); underTest.close(); await().untilAsserted(() -> assertThat(cancelledKey1.get()).isTrue()); // Second computation was cancelled early, because calling the computer await().untilAsserted(() -> assertThat(key2ComputationStarted.getCount()).isEqualTo(1)); verify(computer, times(1)).apply(eq(A_KEY), any()); verifyNoMoreInteractions(computer); } @Test @TakeThreadDumpAfter(seconds = 10) void previously_queued_get_should_receive_latest_value_on_cancellation() throws InterruptedException { var firstComputationStarted = new CountDownLatch(1); var cancelled = new AtomicBoolean(); when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(firstComputationStarted, cancelled)) .thenReturn(ANOTHER_VALUE); // Queue a first computation AtomicReference value = new AtomicReference<>(); var t = new Thread(() -> { value.set(underTest.get(A_KEY)); }); t.start(); firstComputationStarted.await(); // Queue a second computation underTest.refreshAsync(A_KEY); assertThat(underTest.get(A_KEY)).isEqualTo(ANOTHER_VALUE); t.join(); assertThat(value.get()).isEqualTo(ANOTHER_VALUE); assertThat(cancelled.get()).isTrue(); } @Test @TakeThreadDumpAfter(seconds = 10) void should_notify_once_in_case_of_cancellation() throws InterruptedException { var firstComputationStarted = new CountDownLatch(1); when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenAnswer(waitingForCancellation(firstComputationStarted, null)) .thenReturn(ANOTHER_VALUE); underTest.refreshAsync(A_KEY); firstComputationStarted.await(); underTest.refreshAsync(A_KEY); verify(listener, timeout(1000).times(1)).afterCachedValueRefreshed(A_KEY, null, ANOTHER_VALUE); verifyNoMoreInteractions(listener); } @Test void should_notify_once_if_multiple_refresh() { when(computer.apply(eq(A_KEY), any(SonarLintCancelMonitor.class))) .thenReturn(A_VALUE) .thenReturn(ANOTHER_VALUE); assertThat(underTest.get(A_KEY)).isEqualTo(A_VALUE); verify(listener, timeout(1000).times(1)).afterCachedValueRefreshed(A_KEY, null, A_VALUE); underTest.refreshAsync(A_KEY); underTest.refreshAsync(A_KEY); underTest.refreshAsync(A_KEY); underTest.refreshAsync(A_KEY); underTest.refreshAsync(A_KEY); verify(listener, timeout(1000).times(1)).afterCachedValueRefreshed(A_KEY, A_VALUE, ANOTHER_VALUE); verifyNoMoreInteractions(listener); } private static Answer waitingForCancellation(CountDownLatch startedLatch, @Nullable AtomicBoolean wasCancelled) { return invocation -> { var cancelChecker = (SonarLintCancelMonitor) invocation.getArgument(1); startedLatch.countDown(); while (!cancelChecker.isCanceled()) { Thread.sleep(100); } if (wasCancelled != null) { wasCancelled.set(true); } throw new CancellationException(); }; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/AnalyzeFileListRequestHandlerTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import com.google.gson.Gson; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.AnalysisResult; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.analysis.RawIssue; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; class AnalyzeFileListRequestHandlerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); private AnalyzeFileListRequestHandler analyzeFileListRequestHandler; private AnalysisService analysisService; private ClientFileSystemService clientFileSystemService; @BeforeEach void setup() { analysisService = mock(AnalysisService.class); clientFileSystemService = mock(ClientFileSystemService.class); var taintVulnerabilityTrackingService = mock(TaintVulnerabilityTrackingService.class); analyzeFileListRequestHandler = spy(new AnalyzeFileListRequestHandler(analysisService, clientFileSystemService, taintVulnerabilityTrackingService)); } @Test void should_reject_non_post_requests() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.GET, "/analyze"); var response = new BasicClassicHttpResponse(200); var context = mock(HttpContext.class); analyzeFileListRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); } @Test void should_reject_invalid_json_request() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.POST, "/analyze"); request.setEntity(new StringEntity("invalid json", StandardCharsets.UTF_8)); var response = new BasicClassicHttpResponse(200); var context = mock(HttpContext.class); analyzeFileListRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); assertThat(logTester.logs()).contains("Failed to parse analyze file list request"); } @Test void should_reject_null_request_body() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.POST, "/analyze"); request.setEntity(new StringEntity("null", StandardCharsets.UTF_8)); var response = new BasicClassicHttpResponse(200); var context = mock(HttpContext.class); analyzeFileListRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); } @Test void should_reject_empty_file_list() throws HttpException, IOException { var requestJson = new Gson().toJson(new AnalyzeFileListRequestHandler.AnalyzeFileListRequest(Collections.emptyList())); var request = new BasicClassicHttpRequest(Method.POST, "/analyze"); request.setEntity(new StringEntity(requestJson, StandardCharsets.UTF_8)); var response = new BasicClassicHttpResponse(200); var context = mock(HttpContext.class); analyzeFileListRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); } @Test void should_handle_issues_with_null_severity_and_file_path() throws HttpException, IOException { var analysisRequest = new AnalyzeFileListRequestHandler.AnalyzeFileListRequest(List.of("/path/to/file.java")); var requestJson = new Gson().toJson(analysisRequest); var request = new BasicClassicHttpRequest(Method.POST, "/analyze"); request.setEntity(new StringEntity(requestJson, StandardCharsets.UTF_8)); var response = new BasicClassicHttpResponse(200); var context = mock(HttpContext.class); var filesByScope = Map.of("scope1", Set.of(URI.create("file:///path/to/file.java"))); when(clientFileSystemService.groupFilesByConfigScope(anySet())).thenReturn(filesByScope); var mockIssue = createMockRawIssue(); var scanResults = mock(AnalysisResult.class); when(scanResults.rawIssues()).thenReturn(List.of(mockIssue)); when(analysisService.scheduleAnalysis(anyString(), any(UUID.class), anySet(), anyMap(), anyBoolean(), eq(TriggerType.FORCED), any())).thenReturn(CompletableFuture.completedFuture(scanResults)); analyzeFileListRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(200); var responseContent = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8); var analysisResult = new Gson().fromJson(responseContent, AnalyzeFileListRequestHandler.AnalyzeFileListResult.class); assertThat(analysisResult.findings()).hasSize(1); var issue = analysisResult.findings().get(0); assertThat(issue.ruleKey()).isEqualTo("java:S123"); assertThat(issue.severity()).isNull(); assertThat(issue.filePath()).isNull(); assertThat(issue.textRange()).isNull(); } private RawIssue createMockRawIssue() { var mockIssue = mock(RawIssue.class); when(mockIssue.getRuleKey()).thenReturn("java:S123"); when(mockIssue.getMessage()).thenReturn("Test message"); when(mockIssue.getSeverity()).thenReturn(null); when(mockIssue.getFileUri()).thenReturn(null); when(mockIssue.getTextRange()).thenReturn(null); return mockIssue; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/ToggleAutomaticAnalysisRequestHandlerTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server; import com.google.gson.Gson; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoInteractions; class ToggleAutomaticAnalysisRequestHandlerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); private final Gson gson = new Gson(); private AnalysisService analysisService; private ToggleAutomaticAnalysisRequestHandler toggleAutomaticAnalysisRequestHandler; private HttpContext context; @BeforeEach void setup() { analysisService = mock(AnalysisService.class); context = mock(HttpContext.class); toggleAutomaticAnalysisRequestHandler = new ToggleAutomaticAnalysisRequestHandler(analysisService); } @Test void should_reject_non_post_requests() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.GET, "/analysis/automatic/config"); var response = new BasicClassicHttpResponse(200); toggleAutomaticAnalysisRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); verifyNoInteractions(analysisService); } @Test void should_reject_invalid_enabled_parameter() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.POST, "/analysis/automatic/config?invalid=param"); var response = new BasicClassicHttpResponse(200); toggleAutomaticAnalysisRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); verifyNoInteractions(analysisService); } @Test void should_handle_analysis_service_exception() throws HttpException, IOException { var request = new BasicClassicHttpRequest(Method.POST, "/analysis/automatic/config?enabled=true"); var response = new BasicClassicHttpResponse(200); var exception = new RuntimeException("Analysis service failed"); doThrow(exception).when(analysisService).didChangeAutomaticAnalysisSetting(anyBoolean()); toggleAutomaticAnalysisRequestHandler.handle(request, response, context); assertThat(response.getCode()).isEqualTo(HttpStatus.SC_INTERNAL_SERVER_ERROR); assertThat(response.getEntity()).isNotNull(); var responseContent = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8); var errorMessage = gson.fromJson(responseContent, ToggleAutomaticAnalysisRequestHandler.ErrorMessage.class); assertThat(errorMessage.message()).contains("Failed to toggle automatic analysis"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/filter/CspFilterTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import java.io.IOException; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; class CspFilterTest { private CspFilter cspFilter; private HttpFilterChain.ResponseTrigger mockResponseTrigger; private HttpFilterChain mockFilterChain; private HttpContext mockContext; private ClassicHttpRequest mockRequest; @BeforeEach void setUp() { cspFilter = new CspFilter(); mockResponseTrigger = Mockito.mock(HttpFilterChain.ResponseTrigger.class); mockFilterChain = Mockito.mock(HttpFilterChain.class); mockContext = Mockito.mock(HttpContext.class); mockRequest = new BasicClassicHttpRequest("GET", "http://localhost:64120/sonarlint/api/endpoint"); } @Test void it_should_add_csp_header_to_the_response_when_response_is_successful() throws HttpException, IOException { doAnswer(invocation -> { HttpFilterChain.ResponseTrigger trigger = invocation.getArgument(1); var mockResponse = new BasicClassicHttpResponse(200); trigger.submitResponse(mockResponse); trigger.sendInformation(mockResponse); return null; }).when(mockFilterChain).proceed(eq(mockRequest), any(), eq(mockContext)); cspFilter.handle(mockRequest, mockResponseTrigger, mockContext, mockFilterChain); var captor = ArgumentCaptor.forClass(ClassicHttpResponse.class); verify(mockResponseTrigger).submitResponse(captor.capture()); var response = captor.getValue(); var cspHeader = response.getHeader("Content-Security-Policy-Report-Only").getValue(); assertThat(cspHeader).isEqualTo("connect-src 'self' http://localhost:64120;"); verify(mockResponseTrigger).sendInformation(any()); } @ParameterizedTest @ValueSource(strings = {"400", "401", "403", "404", "500"}) void it_should_not_add_csp_header_to_the_response_when_response_is_unsuccessful(String responseCode) throws HttpException, IOException { doAnswer(invocation -> { HttpFilterChain.ResponseTrigger trigger = invocation.getArgument(1); var mockResponse = new BasicClassicHttpResponse(Integer.parseInt(responseCode)); trigger.submitResponse(mockResponse); return null; }).when(mockFilterChain).proceed(eq(mockRequest), any(), eq(mockContext)); cspFilter.handle(mockRequest, mockResponseTrigger, mockContext, mockFilterChain); var captor = ArgumentCaptor.forClass(ClassicHttpResponse.class); verify(mockResponseTrigger).submitResponse(captor.capture()); var response = captor.getValue(); assertThat(response.getHeader("Content-Security-Policy-Report-Only")).isNull(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/filter/RateLimitFilterTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.filter; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.io.HttpFilterChain; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; import static org.mockito.Mockito.*; class RateLimitFilterTests { private final ClassicHttpRequest request = mock(ClassicHttpRequest.class); private final HttpFilterChain.ResponseTrigger responseTrigger = mock(HttpFilterChain.ResponseTrigger.class); private final HttpContext context = mock(HttpContext.class); private final HttpFilterChain chain = mock(HttpFilterChain.class); private RateLimitFilter filter; @BeforeEach void init() { filter = new RateLimitFilter(); } @Test void should_not_proceed_with_request_if_origin_is_null() throws HttpException, IOException { when(request.getHeader("Origin")).thenReturn(null); filter.handle(request, responseTrigger, context, chain); verify(responseTrigger).submitResponse(any()); verify(chain, never()).proceed(any(), any(), any()); } @Test void should_proceed_when_request_is_valid() throws HttpException, IOException { when(request.getHeader("Origin")).thenReturn(new BasicHeader("Origin", "https://example.com")); filter.handle(request, responseTrigger, context, chain); verify(responseTrigger, never()).submitResponse(any()); verify(chain).proceed(any(), any(), any()); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/handler/ShowFixSuggestionRequestHandlerTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.BindingCandidatesFinder; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.event.FixSuggestionReceivedEvent; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranches; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesSynchronizationService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ShowFixSuggestionRequestHandlerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); private ShowFixSuggestionRequestHandler showFixSuggestionRequestHandler; private ConnectionConfigurationRepository connectionConfigurationRepository; private ConfigurationRepository configurationRepository; private SonarLintRpcClient sonarLintRpcClient; private ApplicationEventPublisher eventPublisher; private ClientFile clientFile; private FilePathTranslation filePathTranslation; @BeforeEach void setup() { connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); configurationRepository = mock(ConfigurationRepository.class); var bindingSuggestionProvider = mock(BindingSuggestionProvider.class); var bindingCandidatesFinder = mock(BindingCandidatesFinder.class); sonarLintRpcClient = mock(SonarLintRpcClient.class); filePathTranslation = mock(FilePathTranslation.class); var pathTranslationService = mock(PathTranslationService.class); when(pathTranslationService.getOrComputePathTranslation(any())).thenReturn(Optional.of(filePathTranslation)); var sonarCloudActiveEnvironment = SonarCloudActiveEnvironment.prod(); eventPublisher = mock(ApplicationEventPublisher.class); var sonarProjectBranchesSynchronizationService = mock(SonarProjectBranchesSynchronizationService.class); when(sonarProjectBranchesSynchronizationService.getProjectBranches(any(), any(), any())).thenReturn(new ProjectBranches(Set.of(), "main")); clientFile = mock(ClientFile.class); var clientFs = mock(ClientFileSystemService.class); when(clientFs.getFiles(any())).thenReturn(List.of(clientFile)); var connectionConfiguration = mock(ConnectionConfigurationRepository.class); when(connectionConfiguration.hasConnectionWithOrigin(SonarCloudRegion.EU.getProductionUri().toString())).thenReturn(true); showFixSuggestionRequestHandler = new ShowFixSuggestionRequestHandler(sonarLintRpcClient, eventPublisher, new RequestHandlerBindingAssistant(bindingSuggestionProvider, bindingCandidatesFinder, sonarLintRpcClient, connectionConfigurationRepository, configurationRepository, sonarCloudActiveEnvironment, connectionConfiguration), pathTranslationService, sonarCloudActiveEnvironment, clientFs); } @Test void should_trigger_telemetry() throws URISyntaxException, HttpException, IOException { var request = mock(ClassicHttpRequest.class); when(request.getUri()).thenReturn(URI.create("/sonarlint/api/fix/show" + "?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&branch=branch" + "&organizationKey=sample-organization")); when(request.getMethod()).thenReturn(Method.POST.name()); when(request.getEntity()).thenReturn(new StringEntity(""" { "fileEdit": { "path": "src/main/java/Main.java", "changes": [{ "beforeLineRange": { "startLine": 0, "endLine": 1 }, "before": "", "after": "var fix = 1;" }] }, "suggestionId": "eb93b2b4-f7b0-4b5c-9460-50893968c264", "explanation": "Modifying the variable name is good" } """)); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); when(context.getAttribute(AttributeUtils.PARAMS_ATTRIBUTE)) .thenReturn(Map.of( "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "branch", "branch", "organizationKey", "sample-organization" )); when(context.getAttribute(AttributeUtils.ORIGIN_ATTRIBUTE)) .thenReturn(SonarCloudRegion.EU.getProductionUri().toString()); showFixSuggestionRequestHandler.handle(request, response, context); verify(eventPublisher, times(1)).publishEvent(any(FixSuggestionReceivedEvent.class)); } @Test void should_extract_query_from_sc_request_without_token() throws HttpException, IOException { var request = new BasicClassicHttpRequest("POST", "/sonarlint/api/fix/show" + "?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&branch=branch" + "&organizationKey=sample-organization"); request.setEntity(new StringEntity(""" { "fileEdit": { "path": "src/main/java/Main.java", "changes": [{ "beforeLineRange": { "startLine": 0, "endLine": 1 }, "before": "", "after": "var fix = 1;" }] }, "suggestionId": "eb93b2b4-f7b0-4b5c-9460-50893968c264", "explanation": "Modifying the variable name is good" } """)); var params = Map.of( "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "branch", "branch", "organizationKey", "sample-organization" ); var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request, SonarCloudRegion.EU.getProductionUri().toString(), params); assertThat(showFixSuggestionQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(showFixSuggestionQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(showFixSuggestionQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(showFixSuggestionQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(showFixSuggestionQuery.getBranch()).isEqualTo("branch"); assertThat(showFixSuggestionQuery.getTokenName()).isNull(); assertThat(showFixSuggestionQuery.getTokenValue()).isNull(); assertThat(showFixSuggestionQuery.getFixSuggestion().suggestionId()).isEqualTo("eb93b2b4-f7b0-4b5c-9460-50893968c264"); assertThat(showFixSuggestionQuery.getFixSuggestion().explanation()).isEqualTo("Modifying the variable name is good"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().path()).isEqualTo("src/main/java/Main.java"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).before()).isEmpty(); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).after()).isEqualTo("var fix = 1;"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).beforeLineRange().startLine()).isZero(); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).beforeLineRange().endLine()).isEqualTo(1); } @Test void should_extract_query_from_sc_request_with_token() throws HttpException, IOException { var request = new BasicClassicHttpRequest("POST", "/sonarlint/api/fix/show" + "?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&tokenName=abc" + "&organizationKey=sample-organization" + "&tokenValue=123"); request.setEntity(new StringEntity(""" { "fileEdit": { "path": "src/main/java/Main.java", "changes": [{ "beforeLineRange": { "startLine": 0, "endLine": 1 }, "before": "", "after": "var fix = 1;" }] }, "suggestionId": "eb93b2b4-f7b0-4b5c-9460-50893968c264", "explanation": "Modifying the variable name is good" } """)); var params = Map.of( "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123"); var showFixSuggestionQuery = showFixSuggestionRequestHandler.extractQuery(request, SonarCloudRegion.EU.getProductionUri().toString(), params); assertThat(showFixSuggestionQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(showFixSuggestionQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(showFixSuggestionQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(showFixSuggestionQuery.getTokenName()).isEqualTo("abc"); assertThat(showFixSuggestionQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(showFixSuggestionQuery.getTokenValue()).isEqualTo("123"); assertThat(showFixSuggestionQuery.getFixSuggestion().suggestionId()).isEqualTo("eb93b2b4-f7b0-4b5c-9460-50893968c264"); assertThat(showFixSuggestionQuery.getFixSuggestion().explanation()).isEqualTo("Modifying the variable name is good"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().path()).isEqualTo("src/main/java/Main.java"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).before()).isEmpty(); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).after()).isEqualTo("var fix = 1;"); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).beforeLineRange().startLine()).isZero(); assertThat(showFixSuggestionQuery.getFixSuggestion().fileEdit().changes().get(0).beforeLineRange().endLine()).isEqualTo(1); } @Test void should_validate_fix_suggestion_query_for_sc() { assertThat(new ShowFixSuggestionRequestHandler.ShowFixSuggestionQuery(null, "project", "issue", "branch", "name", "value", "organizationKey", true, generateFixSuggestionPayload()).isValid()).isTrue(); assertThat( new ShowFixSuggestionRequestHandler.ShowFixSuggestionQuery(null, "project", "issue", "branch", "name", "value", null, true, generateFixSuggestionPayload()).isValid()) .isFalse(); } @Test void should_show_fix_suggestion() throws HttpException, IOException { var request = new BasicClassicHttpRequest("POST", "/sonarlint/api/fix/show" + "?project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr" + "&organizationKey=sample-organization"); request.setEntity(new StringEntity(""" { "fileEdit": { "path": "src/main/java/Main.java", "changes": [{ "beforeLineRange": { "startLine": 0, "endLine": 1 }, "before": "", "after": "var fix = 1;" }] }, "suggestionId": "eb93b2b4-f7b0-4b5c-9460-50893968c264", "explanation": "Modifying the variable name is good" } """)); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); when(context.getAttribute(AttributeUtils.PARAMS_ATTRIBUTE)) .thenReturn(Map.of( "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "branch", "branch", "organizationKey", "sample-organization" )); when(context.getAttribute(AttributeUtils.ORIGIN_ATTRIBUTE)) .thenReturn(SonarCloudRegion.EU.getProductionUri().toString()); when(clientFile.getUri()).thenReturn(URI.create("file:///src/main/java/Main.java")); when(filePathTranslation.serverToIdePath(any())).thenReturn(Path.of("src/main/java/Main.java")); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); showFixSuggestionRequestHandler.handle(request, response, context); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> verify(sonarLintRpcClient).showFixSuggestion(any())); } private static ShowFixSuggestionRequestHandler.FixSuggestionPayload generateFixSuggestionPayload() { return new ShowFixSuggestionRequestHandler.FixSuggestionPayload( new ShowFixSuggestionRequestHandler.FileEditPayload( List.of(new ShowFixSuggestionRequestHandler.ChangesPayload( new ShowFixSuggestionRequestHandler.TextRangePayload(0, 1), "before", "after")), "path"), "suggestionId", "explanation"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/embedded/server/handler/ShowIssueRequestHandlerTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.embedded.server.handler; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.protocol.HttpContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.BindingCandidatesFinder; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.SonarCloudActiveEnvironment; import org.sonarsource.sonarlint.core.SonarCloudRegion; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.BoundScope; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.embedded.server.AttributeUtils; import org.sonarsource.sonarlint.core.embedded.server.RequestHandlerBindingAssistant; import org.sonarsource.sonarlint.core.file.FilePathTranslation; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.ShowIssueParams; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.issue.IssueApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranches; import org.sonarsource.sonarlint.core.serverconnection.ProjectBranchesStorage; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.sync.SonarProjectBranchesSynchronizationService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; class ShowIssueRequestHandlerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); private ShowIssueRequestHandler showIssueRequestHandler; private ConnectionConfigurationRepository connectionConfigurationRepository; private ConfigurationRepository configurationRepository; private SonarLintRpcClient sonarLintRpcClient; private ProjectBranchesStorage branchesStorage; private IssueApi issueApi; private TelemetryService telemetryService; @BeforeEach void setup() { connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class); configurationRepository = mock(ConfigurationRepository.class); var bindingSuggestionProvider = mock(BindingSuggestionProvider.class); var bindingCandidatesFinder = mock(BindingCandidatesFinder.class); sonarLintRpcClient = mock(SonarLintRpcClient.class); var filePathTranslation = mock(FilePathTranslation.class); var pathTranslationService = mock(PathTranslationService.class); when(pathTranslationService.getOrComputePathTranslation(any())).thenReturn(Optional.of(filePathTranslation)); var sonarCloudActiveEnvironment = SonarCloudActiveEnvironment.prod(); telemetryService = mock(TelemetryService.class); issueApi = mock(IssueApi.class); var serverApi = mock(ServerApi.class); when(serverApi.issue()).thenReturn(issueApi); var connectionManager = mock(SonarQubeClientManager.class); when(connectionManager.withActiveClientFlatMapOptionalAndReturn(any(), any())).thenAnswer( invocation -> ((Function>) invocation.getArguments()[1]).apply(serverApi)); when(connectionManager.withActiveClientAndReturn(any(), any())).thenAnswer( invocation -> Optional.ofNullable(((Function) invocation.getArguments()[1]).apply(serverApi))); branchesStorage = mock(ProjectBranchesStorage.class); var storageService = mock(StorageService.class); var sonarStorage = mock(SonarProjectStorage.class); var eventPublisher = mock(ApplicationEventPublisher.class); var sonarProjectBranchesSynchronizationService = spy(new SonarProjectBranchesSynchronizationService(storageService, connectionManager, eventPublisher)); doReturn(new ProjectBranches(Set.of(), "main")).when(sonarProjectBranchesSynchronizationService).getProjectBranches(any(), any(), any()); when(storageService.binding(any())).thenReturn(sonarStorage); when(sonarStorage.branches()).thenReturn(branchesStorage); var connectionConfiguration = mock(ConnectionConfigurationRepository.class); when(connectionConfiguration.hasConnectionWithOrigin(SonarCloudRegion.EU.getProductionUri().toString())).thenReturn(true); showIssueRequestHandler = spy(new ShowIssueRequestHandler( sonarLintRpcClient, connectionManager, telemetryService, new RequestHandlerBindingAssistant(bindingSuggestionProvider, bindingCandidatesFinder, sonarLintRpcClient, connectionConfigurationRepository, configurationRepository, sonarCloudActiveEnvironment, connectionConfiguration), pathTranslationService, sonarCloudActiveEnvironment, sonarProjectBranchesSynchronizationService)); } @Test void should_transform_ServerIssueDetail_to_ShowIssueParams() { var connectionId = "connectionId"; var configScopeId = "configScopeId"; var issueKey = "issueKey"; var issueCreationDate = "2023-05-13T17:55:39+0200"; var issueMessage = "issue message"; var issuePath = Paths.get("home/file.java"); var issueRuleKey = "javasecurity:S3649"; var flowLocationPath1 = "home/file_1.java"; var flowLocationPath2 = "home/file_2.java"; var issueTextRange = Common.TextRange.newBuilder().setStartLine(1).setEndLine(2).setStartOffset(3).setEndOffset(4).build(); var locationTextRange1 = Common.TextRange.newBuilder().setStartLine(5).setEndLine(5).setStartOffset(10).setEndOffset(20).build(); var locationTextRange2 = Common.TextRange.newBuilder().setStartLine(50).setEndLine(50).setStartOffset(42).setEndOffset(52).build(); var locationMessage1 = "locationMessage_1"; var locationComponentKey1 = "LocationComponentKey_1"; var locationComponentKey2 = "LocationComponentKey_2"; var locationCodeSnippet1 = "//todo comment"; var issueComponentKey = "IssueComponentKey"; var codeSnippet = "//todo remove this"; when(issueApi.getCodeSnippet(eq(locationComponentKey1), any(), any(), any(), any())).thenReturn(Optional.of(locationCodeSnippet1)); var flow = Common.Flow.newBuilder() .addLocations(Common.Location.newBuilder().setTextRange(locationTextRange1).setComponent(locationComponentKey1).setMsg(locationMessage1)) .addLocations(Common.Location.newBuilder().setTextRange(locationTextRange2).setComponent(locationComponentKey2)) .build(); var issue = Issues.Issue.newBuilder() .setKey(issueKey) .setCreationDate(issueCreationDate) .setRule(issueRuleKey) .setMessage(issueMessage) .setTextRange(issueTextRange) .setComponent(issueComponentKey) .addFlows(flow) .build(); var components = List.of( Issues.Component.newBuilder().setKey(issueComponentKey).setPath(issuePath.toString()).build(), Issues.Component.newBuilder().setKey(locationComponentKey1).setPath(flowLocationPath1).build(), Issues.Component.newBuilder().setKey(locationComponentKey2).setPath(flowLocationPath2).build()); var serverIssueDetails = new IssueApi.ServerIssueDetails(issue, issuePath, components, codeSnippet); var showIssueParams = showIssueRequestHandler.getShowIssueParams(serverIssueDetails, connectionId, configScopeId, "branch", "", new FilePathTranslation(Path.of("ide"), Path.of("home")), new SonarLintCancelMonitor()); assertThat(showIssueParams.getConfigurationScopeId()).isEqualTo(configScopeId); var issueDetails = showIssueParams.getIssueDetails(); assertThat(issueDetails.getIssueKey()).isEqualTo(issueKey); assertThat(issueDetails.getCreationDate()).isEqualTo(issueCreationDate); assertThat(issueDetails.getRuleKey()).isEqualTo(issueRuleKey); assertThat(issueDetails.isTaint()).isTrue(); assertThat(issueDetails.getMessage()).isEqualTo(issueMessage); assertThat(issueDetails.getTextRange().getStartLine()).isEqualTo(1); assertThat(issueDetails.getTextRange().getEndLine()).isEqualTo(2); assertThat(issueDetails.getTextRange().getStartLineOffset()).isEqualTo(3); assertThat(issueDetails.getTextRange().getEndLineOffset()).isEqualTo(4); assertThat(issueDetails.getIdeFilePath()).isEqualTo(Paths.get("ide/file.java")); assertThat(issueDetails.getFlows()).hasSize(1); assertThat(issueDetails.getCodeSnippet()).isEqualTo(codeSnippet); var locations = issueDetails.getFlows().get(0).getLocations(); assertThat(locations).hasSize(2); assertThat(locations.get(0).getTextRange().getStartLine()).isEqualTo(5); assertThat(locations.get(0).getTextRange().getEndLine()).isEqualTo(5); assertThat(locations.get(0).getTextRange().getStartLineOffset()).isEqualTo(10); assertThat(locations.get(0).getTextRange().getEndLineOffset()).isEqualTo(20); assertThat(locations.get(0).getIdeFilePath()).isEqualTo(Paths.get("ide/file_1.java")); assertThat(locations.get(0).getMessage()).isEqualTo(locationMessage1); assertThat(locations.get(0).getCodeSnippet()).isEqualTo(locationCodeSnippet1); assertThat(locations.get(1).getIdeFilePath()).isEqualTo(Paths.get("ide/file_2.java")); assertThat(locations.get(1).getCodeSnippet()).isEmpty(); } @Test void should_trigger_telemetry() throws HttpException, IOException, URISyntaxException { var request = mock(ClassicHttpRequest.class); var params = Map.of("project", "pk", "issue", "ik", "branch", "b", "server", "s"); when(request.getUri()).thenReturn(URI.create("http://localhost:8000/issue?project=pk&issue=ik&branch=b&server=s")); when(request.getMethod()).thenReturn(Method.GET.name()); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); when(context.getAttribute(AttributeUtils.PARAMS_ATTRIBUTE)) .thenReturn(params); when(context.getAttribute(AttributeUtils.ORIGIN_ATTRIBUTE)) .thenReturn("s"); when(issueApi.getCodeSnippet(eq("comp"), any(), any(), any(), any())).thenReturn(Optional.of("snippet")); showIssueRequestHandler.handle(request, response, context); verify(telemetryService).showIssueRequestReceived(); verifyNoMoreInteractions(telemetryService); } @Test void should_extract_query_from_sq_request_with_branch() { var params = Map.of("server", "https://next.sonarqube.com/sonarqube", "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123", "branch", "branch"); var issueQuery = showIssueRequestHandler.extractQuery("https://next.sonarqube.com/", params); assertThat(issueQuery.getServerUrl()).isEqualTo("https://next.sonarqube.com/sonarqube"); assertThat(issueQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(issueQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(issueQuery.getTokenName()).isEqualTo("abc"); assertThat(issueQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(issueQuery.getTokenValue()).isEqualTo("123"); assertThat(issueQuery.getBranch()).isEqualTo("branch"); } @Test void should_extract_query_from_sq_request_without_token() { var params = Map.of("server", "https://next.sonarqube.com/sonarqube", "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "organizationKey", "sample-organization"); var issueQuery = showIssueRequestHandler.extractQuery("https://next.sonarqube.com/", params); assertThat(issueQuery.getServerUrl()).isEqualTo("https://next.sonarqube.com/sonarqube"); assertThat(issueQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(issueQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(issueQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(issueQuery.getTokenName()).isNull(); assertThat(issueQuery.getTokenValue()).isNull(); assertThat(issueQuery.getBranch()).isNull(); } @Test void should_extract_query_from_sq_request_with_token() { var params = Map.of("server", "https://next.sonarqube.com/sonarqube", "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123"); var issueQuery = showIssueRequestHandler.extractQuery("https://next.sonarqube.com/", params); assertThat(issueQuery.getServerUrl()).isEqualTo("https://next.sonarqube.com/sonarqube"); assertThat(issueQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(issueQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(issueQuery.getTokenName()).isEqualTo("abc"); assertThat(issueQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(issueQuery.getTokenValue()).isEqualTo("123"); assertThat(issueQuery.getBranch()).isNull(); } @Test void should_extract_query_from_sc_request_without_token() { var params = Map.of( "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "organizationKey", "sample-organization"); var issueQuery = showIssueRequestHandler.extractQuery(SonarCloudRegion.EU.getProductionUri().toString(), params); assertThat(issueQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(issueQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(issueQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(issueQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(issueQuery.getTokenName()).isNull(); assertThat(issueQuery.getTokenValue()).isNull(); assertThat(issueQuery.getBranch()).isNull(); } @Test void should_extract_query_from_sc_request_with_token() { var params = Map.of("project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123"); var issueQuery = showIssueRequestHandler.extractQuery( SonarCloudRegion.EU.getProductionUri().toString(), params); assertThat(issueQuery.getServerUrl()).isEqualTo("https://sonarcloud.io"); assertThat(issueQuery.getProjectKey()).isEqualTo("org.sonarsource.sonarlint.core:sonarlint-core-parent"); assertThat(issueQuery.getIssueKey()).isEqualTo("AX2VL6pgAvx3iwyNtLyr"); assertThat(issueQuery.getTokenName()).isEqualTo("abc"); assertThat(issueQuery.getOrganizationKey()).isEqualTo("sample-organization"); assertThat(issueQuery.getTokenValue()).isEqualTo("123"); assertThat(issueQuery.getBranch()).isNull(); } @Test void should_validate_issue_query_for_sq() { assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", null, null, null, SonarCloudRegion.US, false).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "", "pullRequest", null, null, null, SonarCloudRegion.EU, false).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", null, "pullRequest", null, null, null, SonarCloudRegion.EU, false).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("", "project", "issue", "branch", "pullRequest", null, null, null, SonarCloudRegion.EU, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "", "issue", "branch", "pullRequest", null, null, null, SonarCloudRegion.EU, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "", "branch", "pullRequest", null, null, null, SonarCloudRegion.EU, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "", "", null, null, null, SonarCloudRegion.EU, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", null, null, null, null, SonarCloudRegion.EU, false).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", "name", null, null, SonarCloudRegion.US, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", null, "value", null, SonarCloudRegion.US, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", "name", "", null, SonarCloudRegion.US, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", "", "value", null, SonarCloudRegion.US, false).isValid()).isFalse(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery("serverUrl", "project", "issue", "branch", "pullRequest", "name", "value", null, SonarCloudRegion.US, false).isValid()) .isTrue(); } @Test void should_validate_issue_query_for_sc() { assertThat(new ShowIssueRequestHandler.ShowIssueQuery(null, "project", "issue", "branch", "pullRequest", "name", "value", "organizationKey", SonarCloudRegion.US, true).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery(null, "project", "issue", "", "pullRequest", "name", "value", "organizationKey", SonarCloudRegion.US, true).isValid()) .isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery(null, "project", "issue", null, "pullRequest", "name", "value", "organizationKey", SonarCloudRegion.US, true).isValid()).isTrue(); assertThat(new ShowIssueRequestHandler.ShowIssueQuery(null, "project", "issue", "branch", "pullRequest", "name", "value", null, SonarCloudRegion.EU, true).isValid()).isFalse(); } @Test void should_detect_taint_issues() { assertThat(ShowIssueRequestHandler.isIssueTaint("java:S1144")).isFalse(); assertThat(ShowIssueRequestHandler.isIssueTaint("javasecurity:S3649")).isTrue(); } @Test void should_find_main_branch_when_branch_is_not_provided() throws HttpException, IOException { var request = new BasicClassicHttpRequest("GET", "/sonarlint/api/issues/show" + "?server=https%3A%2F%2Fnext.sonarqube.com%2Fsonarqube" + "&project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&tokenName=abc" + "&organizationKey=sample-organization" + "&tokenValue=123"); var params = Map.of("server", "https://next.sonarqube.com/sonarqube", "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123"); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); when(context.getAttribute(AttributeUtils.PARAMS_ATTRIBUTE)) .thenReturn(params); when(context.getAttribute(AttributeUtils.ORIGIN_ATTRIBUTE)) .thenReturn(SonarCloudRegion.EU.getProductionUri().toString()); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); when(branchesStorage.exists()).thenReturn(true); when(branchesStorage.read()).thenReturn(new ProjectBranches(Set.of(), "main")); var serverIssueDetails = mock(IssueApi.ServerIssueDetails.class); when(issueApi.fetchServerIssue(any(), any(), any(), any(), any())).thenReturn(Optional.of(serverIssueDetails)); var issueDetails = mock(IssueDetailsDto.class); doReturn(new ShowIssueParams("configScope", issueDetails)).when(showIssueRequestHandler).getShowIssueParams(any(), any(), any(), any(), any(), any(), any()); showIssueRequestHandler.handle(request, response, context); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> verify(sonarLintRpcClient).showIssue(any())); } @Test void should_find_main_branch_when_not_provided_and_not_stored() throws HttpException, IOException { var request = new BasicClassicHttpRequest("GET", "/sonarlint/api/issues/show" + "?server=https%3A%2F%2Fnext.sonarqube.com%2Fsonarqube" + "&project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&tokenName=abc" + "&organizationKey=sample-organization" + "&tokenValue=123"); var params = Map.of("server", "https://next.sonarqube.com/sonarqube", "project", "org.sonarsource.sonarlint.core:sonarlint-core-parent", "issue", "AX2VL6pgAvx3iwyNtLyr", "tokenName", "abc", "organizationKey", "sample-organization", "tokenValue", "123"); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); when(context.getAttribute(AttributeUtils.PARAMS_ATTRIBUTE)) .thenReturn(params); when(context.getAttribute(AttributeUtils.ORIGIN_ATTRIBUTE)) .thenReturn(SonarCloudRegion.EU.getProductionUri().toString()); when(connectionConfigurationRepository.findByOrganization(any())).thenReturn(List.of( new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "name", "organizationKey", SonarCloudRegion.EU, false))); when(configurationRepository.getBoundScopesToConnectionAndSonarProject(any(), any())).thenReturn(List.of(new BoundScope("configScope", "connectionId", "projectKey"))); when(branchesStorage.exists()).thenReturn(false); var serverIssueDetails = mock(IssueApi.ServerIssueDetails.class); when(issueApi.fetchServerIssue(any(), any(), any(), any(), any())).thenReturn(Optional.of(serverIssueDetails)); var issueDetails = mock(IssueDetailsDto.class); doReturn(new ShowIssueParams("configScope", issueDetails)).when(showIssueRequestHandler).getShowIssueParams(any(), any(), any(), any(), any(), any(), any()); showIssueRequestHandler.handle(request, response, context); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> verify(sonarLintRpcClient).showIssue(any())); } @Test void should_verify_missing_origin() throws HttpException, IOException { var request = new BasicClassicHttpRequest("GET", "/sonarlint/api/issues/show" + "?server=https%3A%2F%2Fnext.sonarqube.com%2Fsonarqube" + "&project=org.sonarsource.sonarlint.core%3Asonarlint-core-parent" + "&issue=AX2VL6pgAvx3iwyNtLyr&tokenName=abc" + "&organizationKey=sample-organization" + "&tokenValue=123"); var response = mock(ClassicHttpResponse.class); var context = mock(HttpContext.class); showIssueRequestHandler.handle(request, response, context); verifyNoMoreInteractions(sonarLintRpcClient); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/file/FilePathTranslationTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import java.nio.file.Path; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; class FilePathTranslationTests { @Test void serverToPathTranslation() { var underTest = new FilePathTranslation(Path.of("/foo"), Path.of("/bar")); assertThat(underTest.serverToIdePath(Path.of("/baz"))).isEqualTo(Path.of("/baz")); assertThat(underTest.serverToIdePath(Path.of("/bar/baz"))).isEqualTo(Path.of("/foo/baz")); } @Test void serverToPathTranslationWhenPrefixIsEmpty() { var underTest = new FilePathTranslation(Path.of("ide"), Path.of("")); assertThat(underTest.serverToIdePath(Path.of("baz"))).isEqualTo(Path.of("ide/baz")); assertThat(underTest.serverToIdePath(Path.of("bar/baz"))).isEqualTo(Path.of("ide/bar/baz")); } @Test void ideToServerPathTranslation() { var underTest = new FilePathTranslation(Path.of("/foo"), Path.of("/bar")); assertThat(underTest.ideToServerPath(Path.of("/baz"))).isEqualTo(Path.of("/baz")); assertThat(underTest.ideToServerPath(Path.of("/foo/baz"))).isEqualTo(Path.of("/bar/baz")); } @Test void ideToServerPathTranslationWhenPrefixIsEmpty() { var underTest = new FilePathTranslation(Path.of(""), Path.of("server")); assertThat(underTest.ideToServerPath(Path.of("baz"))).isEqualTo(Path.of("server/baz")); assertThat(underTest.ideToServerPath(Path.of("foo/baz"))).isEqualTo(Path.of("server/foo/baz")); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/file/PathTranslationServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.fs.ClientFile; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PathTranslationServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String CONFIG_SCOPE = "configScopeA"; private static final Binding BINDING = new Binding("connectionA", "sonarProjectA"); private final ClientFileSystemService clientFs = mock(ClientFileSystemService.class); private final ConfigurationRepository configurationRepository = mock(ConfigurationRepository.class); private final ServerFilePathsProvider serverFilePathsProvider = mock(ServerFilePathsProvider.class); private final PathTranslationService underTest = new PathTranslationService(clientFs, configurationRepository, serverFilePathsProvider); @BeforeEach void prepare() { when(configurationRepository.getEffectiveBinding(CONFIG_SCOPE)).thenReturn(Optional.of(BINDING)); } @Test void shouldComputePathTranslations() { mockServerFilePaths(BINDING, "moduleA/src/Foo.java"); mockClientFilePaths("src/Foo.java"); var result = underTest.getOrComputePathTranslation(CONFIG_SCOPE); assertThat(result).isPresent(); assertThat(result.get()) .usingRecursiveComparison() .isEqualTo(new FilePathTranslation(Paths.get(""), Paths.get("moduleA"))); } private void mockServerFilePaths(Binding binding, String... paths) { when(serverFilePathsProvider.getServerPaths(eq(binding), any(SonarLintCancelMonitor.class))) .thenReturn(Optional.of(Arrays.stream(paths).map(Paths::get).toList())); } @Test void shouldCachePathTranslations() { mockServerFilePaths(BINDING, "moduleA/src/Foo.java"); mockClientFilePaths("src/Foo.java"); var result1 = underTest.getOrComputePathTranslation(CONFIG_SCOPE); assertThat(result1).isPresent(); assertThat(result1.get()) .usingRecursiveComparison() .isEqualTo(new FilePathTranslation(Paths.get(""), Paths.get("moduleA"))); var result2 = underTest.getOrComputePathTranslation(CONFIG_SCOPE); assertThat(result2).isPresent(); assertThat(result2.get()) .usingRecursiveComparison() .isEqualTo(new FilePathTranslation(Paths.get(""), Paths.get("moduleA"))); verify(clientFs, times(1)).getFiles(any()); } @Test void shouldRecomputePathTranslationsAfterBindingChange() { mockServerFilePaths(BINDING, "moduleA/src/Foo.java"); mockClientFilePaths("src/Foo.java"); var result1 = underTest.getOrComputePathTranslation(CONFIG_SCOPE); assertThat(result1).isPresent(); assertThat(result1.get()) .usingRecursiveComparison() .isEqualTo(new FilePathTranslation(Paths.get(""), Paths.get("moduleA"))); Binding newBinding = mock(Binding.class); when(configurationRepository.getEffectiveBinding(CONFIG_SCOPE)).thenReturn(Optional.of(newBinding)); mockServerFilePaths(newBinding, "moduleB/src/Foo.java"); underTest.onBindingChanged(new BindingConfigChangedEvent(CONFIG_SCOPE, null, null)); var result2 = underTest.getOrComputePathTranslation(CONFIG_SCOPE); assertThat(result2).isPresent(); assertThat(result2.get()) .usingRecursiveComparison() .isEqualTo(new FilePathTranslation(Paths.get(""), Paths.get("moduleB"))); } private void mockClientFilePaths(String... paths) { doReturn(Arrays.stream(paths) .map(path -> new ClientFile(null, null, Paths.get(path), null, null, null, null, true)) .toList()) .when(clientFs) .getFiles(CONFIG_SCOPE); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/file/ServerFilePathsProviderTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.file; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.component.ComponentApi; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; class ServerFilePathsProviderTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String CONNECTION_A = "connection_A"; private static final String CONNECTION_B = "connection_B"; public static final String PROJECT_KEY = "projectKey"; private Path cacheDirectory; private final SonarQubeClientManager sonarQubeClientManager = mock(SonarQubeClientManager.class); private final ServerApi serverApi_A = mock(ServerApi.class); private final ServerApi serverApi_B = mock(ServerApi.class); private final SonarLintCancelMonitor cancelMonitor = mock(SonarLintCancelMonitor.class); private final ComponentApi componentApi_A = mock(ComponentApi.class); private final ComponentApi componentApi_B = mock(ComponentApi.class); private ServerFilePathsProvider underTest; @BeforeEach void before(@TempDir Path storageDir) throws IOException { cacheDirectory = storageDir.resolve("cache"); Files.createDirectories(cacheDirectory); when(serverApi_A.component()).thenReturn(componentApi_A); when(serverApi_B.component()).thenReturn(componentApi_B); when(sonarQubeClientManager.withActiveClientAndReturn(eq(CONNECTION_A), any())).thenAnswer( invocation -> Optional.ofNullable(((Function) invocation.getArguments()[1]).apply(serverApi_A))); when(sonarQubeClientManager.withActiveClientAndReturn(eq(CONNECTION_B), any())).thenAnswer( invocation -> Optional.ofNullable(((Function) invocation.getArguments()[1]).apply(serverApi_B))); mockServerFilePaths(componentApi_A, "pathA", "pathB"); mockServerFilePaths(componentApi_B, "pathC", "pathD"); var userPaths = mock(UserPaths.class); when(userPaths.getStorageRoot()).thenReturn(storageDir); underTest = new ServerFilePathsProvider(sonarQubeClientManager, userPaths); cacheDirectory = storageDir.resolve("cache"); } @Test void clear_cache_directory_after_initialization(@TempDir Path storageDir) throws IOException { cacheDirectory = storageDir.resolve("cache"); Files.createDirectories(cacheDirectory); assertThat(cacheDirectory.toFile()).exists(); var userPaths = mock(UserPaths.class); when(userPaths.getStorageRoot()).thenReturn(storageDir); new ServerFilePathsProvider(null, userPaths); assertThat(cacheDirectory.toFile()).doesNotExist(); } @Test void write_to_cache_file_after_fetch() throws IOException { underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); assertThat(cacheDirectory.toFile().listFiles()).hasSize(1); File file = Objects.requireNonNull(cacheDirectory.toFile().listFiles())[0]; List paths = FileUtils.readLines(file, Charset.defaultCharset()); assertThat(paths).hasSize(2); assertThat(paths.get(0)).isEqualTo("pathA"); assertThat(paths.get(1)).isEqualTo("pathB"); } @Test void fetch_from_in_memory_for_the_second_attempt() throws IOException { underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); verify(componentApi_A, times(1)).getAllFileKeys(PROJECT_KEY, cancelMonitor); verifyNoMoreInteractions(componentApi_A); FileUtils.deleteDirectory(cacheDirectory.toFile()); underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); assertThat(cacheDirectory.toFile()).doesNotExist(); } @Test void fetch_from_file_when_cache_timeout() throws IOException { underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); File file = Objects.requireNonNull(cacheDirectory.toFile().listFiles())[0]; BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true)); bufferedWriter.write("NewPath"); bufferedWriter.newLine(); bufferedWriter.close(); underTest.clearInMemoryCache(); List paths = underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor).get(); assertThat(paths).hasSize(3); assertThat(paths.get(0)).hasToString("pathA"); assertThat(paths.get(1)).hasToString("pathB"); assertThat(paths.get(2)).hasToString("NewPath"); } @Test void write_to_two_cache_files_for_different_request() { underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); underTest.getServerPaths(new Binding(CONNECTION_B, PROJECT_KEY), cancelMonitor); assertThat(cacheDirectory.toFile().listFiles()).hasSize(2); } @Test void shouldLogAndIgnoreOtherErrors() { when(serverApi_A.component().getAllFileKeys(PROJECT_KEY, cancelMonitor)).thenAnswer(invocation -> { throw new IllegalStateException(); }); underTest.getServerPaths(new Binding(CONNECTION_A, PROJECT_KEY), cancelMonitor); assertThat(logTester.logs()) .contains("Error while getting server file paths for project 'projectKey'"); } private void mockServerFilePaths(ComponentApi componentApi, String... paths) { doReturn(Arrays.stream(paths).map(path -> PROJECT_KEY + ":" + path).toList()) .when(componentApi) .getAllFileKeys(PROJECT_KEY, cancelMonitor); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/fs/ClientFileTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ClientFileTests { @Test void dirty_file_larger_than_threshold_returns_true() throws Exception { var uri = URI.create("file:///dirty.js"); var clientFile = new ClientFile(uri, "scope", Paths.get("dirty.js"), null, StandardCharsets.UTF_8, null, null, true); var content = "x".repeat(2048); clientFile.setDirty(content); assertThat(clientFile.isLargerThan(1024)).isTrue(); assertThat(clientFile.isLargerThan(4096)).isFalse(); } @Test void clean_local_file_uses_files_size_and_non_local_returns_false() throws Exception { var tempFile = Files.createTempFile("sl-clientfile-size", ".txt"); Files.write(tempFile, new byte[4096]); var localUri = tempFile.toUri(); var localClientFile = new ClientFile(localUri, "scope", Paths.get("local.txt"), null, StandardCharsets.UTF_8, tempFile, null, true); var missingPath = tempFile.getParent().resolve("missing.txt"); var nonLocalUri = missingPath.toUri(); var nonLocalClientFile = new ClientFile(nonLocalUri, "scope", Paths.get("missing.txt"), null, StandardCharsets.UTF_8, null, null, true); assertThat(localClientFile.isLargerThan(1024)).isTrue(); assertThat(localClientFile.isLargerThan(8192)).isFalse(); assertThat(nonLocalClientFile.isLargerThan(1)).isFalse(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/fs/FileExclusionServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.fs; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent; import org.sonarsource.sonarlint.core.file.PathTranslationService; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetFileExclusionsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetFileExclusionsResponse; import org.sonarsource.sonarlint.core.serverconnection.AnalyzerConfigurationStorage; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class FileExclusionServiceTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private ConfigurationRepository configRepo; private StorageService storageService; private ClientFileSystemService clientFileSystemService; private FileExclusionService underTest; private SonarLintRpcClient client; @BeforeEach void setup() { configRepo = mock(ConfigurationRepository.class); storageService = mock(StorageService.class); var pathTranslationService = mock(PathTranslationService.class); clientFileSystemService = mock(ClientFileSystemService.class); client = mock(SonarLintRpcClient.class); underTest = new FileExclusionService(configRepo, storageService, pathTranslationService, clientFileSystemService, client); } @Test void should_return_false_and_log_warning_when_analyzer_storage_is_not_valid() { var fileUri = URI.create("file:///path/to/file.java"); var configScopeId = "configScope1"; var connectionId = "connectionId"; var projectKey = "projectKey"; var clientFile = mock(ClientFile.class); when(clientFile.getConfigScopeId()).thenReturn(configScopeId); var binding = new Binding(connectionId, projectKey); var connectionStorage = mock(ConnectionStorage.class); var projectStorage = mock(SonarProjectStorage.class); var analyzerStorage = mock(AnalyzerConfigurationStorage.class); // Setup the mocks to return the invalid analyzer storage when(clientFileSystemService.getClientFile(fileUri)).thenReturn(clientFile); when(configRepo.getEffectiveBinding(configScopeId)).thenReturn(Optional.of(binding)); when(storageService.connection(connectionId)).thenReturn(connectionStorage); when(connectionStorage.project(projectKey)).thenReturn(projectStorage); when(projectStorage.analyzerConfiguration()).thenReturn(analyzerStorage); when(analyzerStorage.isValid()).thenReturn(false); // This is the key setup for our test var cancelMonitor = mock(SonarLintCancelMonitor.class); var result = underTest.computeIfExcluded(fileUri, cancelMonitor); assertThat(result).isFalse(); assertThat(logTester.logs()).contains("Unable to read settings in local storage, analysis storage is not ready"); } @Test void should_return_false_when_no_client_file_found() { var fileUri = URI.create("file:///path/to/nonexistent.java"); var cancelMonitor = mock(SonarLintCancelMonitor.class); when(clientFileSystemService.getClientFile(fileUri)).thenReturn(null); var result = underTest.computeIfExcluded(fileUri, cancelMonitor); assertThat(result).isFalse(); assertThat(logTester.logs()).contains("Unable to find client file for uri file:///path/to/nonexistent.java"); } @Test void should_return_false_when_no_effective_binding() { var fileUri = URI.create("file:///path/to/file.java"); var configScopeId = "configScope1"; var cancelMonitor = mock(SonarLintCancelMonitor.class); var clientFile = mock(ClientFile.class); when(clientFile.getConfigScopeId()).thenReturn(configScopeId); when(clientFileSystemService.getClientFile(fileUri)).thenReturn(clientFile); when(configRepo.getEffectiveBinding(configScopeId)).thenReturn(Optional.empty()); var result = underTest.computeIfExcluded(fileUri, cancelMonitor); assertThat(result).isFalse(); } @Test void should_filter_out_files_exceeding_5mb() throws IOException { var configScopeId = "scope"; var baseDir = Files.createTempDirectory("sl-auto-size-base"); // Create a small file (~10 KB) and a large file (~6 MB) var smallFile = baseDir.resolve("small.js"); var largeFile = baseDir.resolve("large.js"); Files.write(smallFile, new byte[10 * 1024]); Files.write(largeFile, new byte[6 * 1024 * 1024]); var smallUri = smallFile.toUri(); var largeUri = largeFile.toUri(); var smallClientFile = mock(ClientFile.class); when(smallClientFile.getUri()).thenReturn(smallUri); when(smallClientFile.getClientRelativePath()).thenReturn(Paths.get("small.js")); when(smallClientFile.isUserDefined()).thenReturn(true); var largeClientFile = mock(ClientFile.class); when(largeClientFile.getUri()).thenReturn(largeUri); when(largeClientFile.getClientRelativePath()).thenReturn(Paths.get("large.js")); when(largeClientFile.isUserDefined()).thenReturn(true); when(clientFileSystemService.getClientFiles(configScopeId, smallUri)).thenReturn(smallClientFile); when(clientFileSystemService.getClientFiles(configScopeId, largeUri)).thenReturn(largeClientFile); when(client.getFileExclusions(any(GetFileExclusionsParams.class))).thenReturn(CompletableFuture.completedFuture(new GetFileExclusionsResponse(Collections.emptySet()))); when(smallClientFile.isLargerThan(anyLong())).thenReturn(false); when(largeClientFile.isLargerThan(anyLong())).thenReturn(true); // Avoid interference from server-side exclusions var spy = Mockito.spy(underTest); Mockito.doReturn(false).when(spy).isExcludedFromServer(any(URI.class)); var result = spy.filterOutExcludedFiles(configScopeId, baseDir, Set.of(smallUri, largeUri)); assertThat(result).extracting(ClientFile::getUri).containsExactlyInAnyOrder(smallUri); assertThat(logTester.logs()).anySatisfy(s -> assertThat(s).contains("Filtered out URIs exceeding max allowed size")); } @Test void should_refresh_exclusions_for_inherited_descendant_scopes_when_binding_changes() { var rootScope = "rootScope"; var childScope = "childScope"; var parentUri = URI.create("file:///p/Foo.java"); var childUri = URI.create("file:///p/module/Bar.java"); when(configRepo.getChildrenWithInheritedBinding(rootScope)).thenReturn(List.of(childScope)); var parentFile = mock(ClientFile.class); when(parentFile.getUri()).thenReturn(parentUri); var childFile = mock(ClientFile.class); when(childFile.getUri()).thenReturn(childUri); when(clientFileSystemService.getFiles(rootScope)).thenReturn(List.of(parentFile)); when(clientFileSystemService.getFiles(childScope)).thenReturn(List.of(childFile)); var connectionStorage = mock(ConnectionStorage.class); var projectStorage = mock(SonarProjectStorage.class); var analyzerStorage = mock(AnalyzerConfigurationStorage.class); when(storageService.connection("conn")).thenReturn(connectionStorage); when(connectionStorage.project("pk")).thenReturn(projectStorage); when(projectStorage.analyzerConfiguration()).thenReturn(analyzerStorage); when(analyzerStorage.isValid()).thenReturn(true); var event = new BindingConfigChangedEvent(rootScope, BindingConfiguration.noBinding(false), new BindingConfiguration("conn", "pk", false)); underTest.onBindingChanged(event); verify(clientFileSystemService).getFiles(rootScope); verify(clientFileSystemService).getFiles(childScope); } @Test void should_clear_exclusions_for_inherited_descendant_scopes_when_binding_removed() { var rootScope = "rootScope"; var childScope = "childScope"; var parentUri = URI.create("file:///p/Foo.java"); var childUri = URI.create("file:///p/module/Bar.java"); when(configRepo.getChildrenWithInheritedBinding(rootScope)).thenReturn(List.of(childScope)); var parentFile = mock(ClientFile.class); when(parentFile.getUri()).thenReturn(parentUri); var childFile = mock(ClientFile.class); when(childFile.getUri()).thenReturn(childUri); when(clientFileSystemService.getFiles(rootScope)).thenReturn(List.of(parentFile)); when(clientFileSystemService.getFiles(childScope)).thenReturn(List.of(childFile)); var event = new BindingConfigChangedEvent(rootScope, new BindingConfiguration("conn", "pk", false), BindingConfiguration.noBinding(false)); underTest.onBindingChanged(event); verify(clientFileSystemService).getFiles(rootScope); verify(clientFileSystemService).getFiles(childScope); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/hotspot/HotspotServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.hotspot; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import static org.assertj.core.api.Assertions.assertThat; class HotspotServiceTests { @Test void testBuildSonarQubeHotspotUrl() { assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("http://foo.com", "", false, null))) .isEqualTo("http://foo.com/security_hotspots?id=myProject&branch=myBranch&hotspots=hotspotKey"); } @Test void testBuildSonarCloudHotspotUrl() { assertThat(HotspotService.buildHotspotUrl("myProject", "myBranch", "hotspotKey", new EndpointParams("https://sonarcloud.io", "", true, "myOrg"))) .isEqualTo("https://sonarcloud.io/project/security_hotspots?id=myProject&branch=myBranch&hotspots=hotspotKey"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/issue/matching/IssueMatcherTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.issue.matching; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.tracking.matching.IssueMatcher; import org.sonarsource.sonarlint.core.tracking.matching.MatchingAttributesMapper; import static org.assertj.core.api.Assertions.assertThat; class IssueMatcherTests { private IssueMatcher underTest; private static class FakeIssueType { private String ruleKey = "dummy rule key"; private Integer line; private String textRangeHash; private String lineHash; private String message = "dummy message"; private String serverKey; public FakeIssueType setRuleKey(String ruleKey) { this.ruleKey = ruleKey; return this; } public FakeIssueType setLine(Integer line) { this.line = line; return this; } public FakeIssueType setTextRangeHash(String hash) { this.textRangeHash = hash; return this; } public FakeIssueType setLineHash(String hash) { this.lineHash = hash; return this; } public FakeIssueType setMessage(String message) { this.message = message; return this; } public FakeIssueType setServerKey(String key) { this.serverKey = key; return this; } } private static class FakeIssueMatchingAttributeMapper implements MatchingAttributesMapper { @Override public String getRuleKey(FakeIssueType issue) { return issue.ruleKey; } @Override public Optional getLine(FakeIssueType issue) { return Optional.ofNullable(issue.line); } @Override public Optional getTextRangeHash(FakeIssueType issue) { return Optional.ofNullable(issue.textRangeHash); } @Override public Optional getLineHash(FakeIssueType issue) { return Optional.ofNullable(issue.lineHash); } @Override public String getMessage(FakeIssueType issue) { return issue.message; } @Override public Optional getServerIssueKey(FakeIssueType issue) { return Optional.ofNullable(issue.serverKey); } } @Test void should_not_match_issues_with_different_rule_key() { var issueForRuleA = new FakeIssueType().setRuleKey("ruleA"); var issueForRuleB = new FakeIssueType().setRuleKey("ruleB"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(issueForRuleB)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(issueForRuleA)); assertThat(result.getMatchedLefts()).isEmpty(); } @Test void should_match_by_line_and_text_range_hash() { var baseIssue = new FakeIssueType().setLine(7).setTextRangeHash("same range hash"); var differentLine = new FakeIssueType().setLine(8).setTextRangeHash("same range hash"); var differentTextRangeHash = new FakeIssueType().setLine(7).setTextRangeHash("different range hash"); var differentBoth = new FakeIssueType().setLine(8).setTextRangeHash("different range hash"); var same = new FakeIssueType().setLine(7).setTextRangeHash("same range hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine, differentTextRangeHash, differentBoth, same)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(same)).isEqualTo(baseIssue); } @Test void should_match_by_line_and_line_hash_even_if_different_message_and_text_range() { var baseIssue = new FakeIssueType().setLine(7).setLineHash("same line hash").setMessage("different message").setTextRangeHash("different range hash"); var differentLine = new FakeIssueType().setLine(8).setLineHash("same line hash"); var differentLineHash = new FakeIssueType().setLine(7).setLineHash("different line hash"); var differentBoth = new FakeIssueType().setLine(8).setLineHash("different line hash"); var same = new FakeIssueType().setLine(7).setLineHash("same line hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine, differentLineHash, differentBoth, same)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(same)).isEqualTo(baseIssue); } @Test void should_match_by_line_and_message_even_if_different_hash() { var baseIssue = new FakeIssueType().setLine(7).setMessage("same message").setTextRangeHash("different range hash"); var differentLine = new FakeIssueType().setLine(8).setMessage("same message"); var differentMessage = new FakeIssueType().setLine(7).setMessage("different message"); var differentBoth = new FakeIssueType().setLine(8).setMessage("different message"); var same = new FakeIssueType().setLine(7).setMessage("same message"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine, differentMessage, differentBoth, same)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(same)).isEqualTo(baseIssue); } @Test void should_match_by_text_range_hash_even_if_no_line_number_before() { var baseIssueWithNoLine = new FakeIssueType().setTextRangeHash("same range hash"); var differentLine = new FakeIssueType().setLine(8).setTextRangeHash("same range hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssueWithNoLine)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssueWithNoLine); } @Test void should_match_by_text_range_hash_even_if_different_line_number() { var baseIssue = new FakeIssueType().setLine(7).setTextRangeHash("same range hash"); var differentLine = new FakeIssueType().setLine(8).setTextRangeHash("same range hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssue); } @Test void should_match_by_line_hash_even_if_no_line_number_before() { var baseIssueWithNoLine = new FakeIssueType().setLineHash("same line hash"); var differentLine = new FakeIssueType().setLine(8).setLineHash("same line hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssueWithNoLine)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssueWithNoLine); } @Test void should_match_by_line_hash_even_if_different_line_number() { var baseIssue = new FakeIssueType().setLine(7).setLineHash("same line hash"); var differentLine = new FakeIssueType().setLine(8).setLineHash("same line hash"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssue); } @Test void should_match_by_serverKey_even_if_no_line_number_before() { var baseIssueWithNoLine = new FakeIssueType().setServerKey("same key"); var differentLine = new FakeIssueType().setLine(8).setServerKey("same key"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssueWithNoLine)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssueWithNoLine); } @Test void should_match_by_serverKey_even_if_different_line_number() { var baseIssue = new FakeIssueType().setLine(7).setServerKey("same key"); var differentLine = new FakeIssueType().setLine(8).setServerKey("same key"); underTest = new IssueMatcher<>(new FakeIssueMatchingAttributeMapper(), List.of(baseIssue)); var result = underTest.matchWith(new FakeIssueMatchingAttributeMapper(), List.of(differentLine)); assertThat(result.getMatchedLefts()).hasSize(1); assertThat(result.getMatch(differentLine)).isEqualTo(baseIssue); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/monitoring/MonitoringUserIdStoreTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.monitoring; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class MonitoringUserIdStoreTests { @RegisterExtension static final SonarLintLogTester logTester = new SonarLintLogTester(); private Path userHome; private Path userIdFilePath; @BeforeEach void setUp(@TempDir Path temp) { userHome = temp; userIdFilePath = userHome.resolve("id"); } private MonitoringUserIdStore createStore() { var userPaths = mock(UserPaths.class); when(userPaths.getUserHome()).thenReturn(userHome); return new MonitoringUserIdStore(userPaths); } @Test void should_create_file_and_uuid_on_first_call() throws IOException { var store = createStore(); assertThat(userIdFilePath).doesNotExist(); var userId = store.getOrCreate(); assertThat(userId).isPresent(); assertThat(userIdFilePath).exists(); var decodedContent = new String(Base64.getDecoder().decode(Files.readString(userIdFilePath)), StandardCharsets.UTF_8); assertThat(decodedContent).isEqualTo(userId.get().toString()); } @Test void should_reuse_existing_uuid() throws IOException { var existingUuid = UUID.randomUUID(); writeEncodedUuid(existingUuid); var store = createStore(); var userId = store.getOrCreate(); assertThat(userId) .isPresent() .contains(existingUuid); } @Test void should_return_cached_uuid_on_subsequent_calls() { var store = createStore(); var firstCall = store.getOrCreate(); var secondCall = store.getOrCreate(); assertThat(firstCall).isPresent(); assertThat(secondCall).isPresent(); assertThat(firstCall).contains(secondCall.get()); } @Test void should_overwrite_invalid_content_with_new_uuid() throws IOException { Files.writeString(userIdFilePath, "not-a-valid-base64-or-uuid"); var store = createStore(); var userId = store.getOrCreate(); assertThat(userId).isPresent(); var decodedContent = new String(Base64.getDecoder().decode(Files.readString(userIdFilePath)), StandardCharsets.UTF_8); assertThat(decodedContent).isEqualTo(userId.get().toString()); } @Test void should_overwrite_empty_file_with_new_uuid() throws IOException { Files.writeString(userIdFilePath, ""); var store = createStore(); var userId = store.getOrCreate(); assertThat(userId).isPresent(); var decodedContent = new String(Base64.getDecoder().decode(Files.readString(userIdFilePath)), StandardCharsets.UTF_8); assertThat(decodedContent).isEqualTo(userId.get().toString()); } @Test void should_trim_whitespace_when_reading_uuid() throws IOException { var existingUuid = UUID.randomUUID(); var encoded = Base64.getEncoder().encodeToString((" " + existingUuid.toString() + "\n ").getBytes(StandardCharsets.UTF_8)); Files.writeString(userIdFilePath, encoded); var store = createStore(); var userId = store.getOrCreate(); assertThat(userId) .isPresent() .contains(existingUuid); } private void writeEncodedUuid(UUID uuid) throws IOException { var encoded = Base64.getEncoder().encodeToString(uuid.toString().getBytes(StandardCharsets.UTF_8)); Files.writeString(userIdFilePath, encoded); } @Test void concurrent_calls_should_return_same_uuid() { var store = createStore(); int numberOfThreads = 10; var executorService = Executors.newFixedThreadPool(numberOfThreads); CountDownLatch latch = new CountDownLatch(1); List> futures = new ArrayList<>(); IntStream.range(0, numberOfThreads).forEach(i -> { futures.add(executorService.submit(() -> { try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return store.getOrCreate().orElse(null); })); }); latch.countDown(); var results = futures.stream().map(f -> { try { return f.get(); } catch (ExecutionException e) { fail(e.getCause()); return null; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } }).toList(); assertThat(results) .hasSize(numberOfThreads) .allMatch(uuid -> uuid != null && uuid.equals(results.get(0))); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/newcode/NewCodeServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.newcode; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.GetNewCodeDefinitionResponse; import org.sonarsource.sonarlint.core.serverconnection.SonarProjectStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.NewCodeDefinitionStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class NewCodeServiceTests { private ConfigurationRepository mockConfigRepository; private StorageService mockStorageService; private NewCodeService underTest; @BeforeEach void setup() { mockConfigRepository = mock(ConfigurationRepository.class); mockStorageService = mock(StorageService.class); underTest = new NewCodeService(mockConfigRepository, mockStorageService, mock(TelemetryService.class)); } @Test void getNewCodeDefinition_noBinding() { var ncd = underTest.getNewCodeDefinition("scope"); assertThat(ncd).extracting(GetNewCodeDefinitionResponse::getDescription, GetNewCodeDefinitionResponse::isSupported) .containsExactly("From last 30 days", true); } @Test void getNewCodeDefinition_noNcdSynchronized() { String scopeId = "scope"; var effectiveBinding = mock(Binding.class); when(mockConfigRepository.getEffectiveBinding(scopeId)) .thenReturn(Optional.of(effectiveBinding)); var storage = mock(SonarProjectStorage.class); when(mockStorageService.binding(effectiveBinding)) .thenReturn(storage); var newCodeDefStorage = mock(NewCodeDefinitionStorage.class); when(storage.newCodeDefinition()).thenReturn(newCodeDefStorage); var ncd = underTest.getNewCodeDefinition("scope"); assertThat(ncd).extracting(GetNewCodeDefinitionResponse::getDescription, GetNewCodeDefinitionResponse::isSupported) .containsExactly("No new code definition found", false); } @Test void getNewCodeDefinition_readFromStorage() { String scopeId = "scope"; var effectiveBinding = mock(Binding.class); when(mockConfigRepository.getEffectiveBinding(scopeId)) .thenReturn(Optional.of(effectiveBinding)); var storage = mock(SonarProjectStorage.class); when(mockStorageService.binding(effectiveBinding)) .thenReturn(storage); var newCodeDefStorage = mock(NewCodeDefinitionStorage.class); when(storage.newCodeDefinition()).thenReturn(newCodeDefStorage); var newCodeDefinition = NewCodeDefinition.withNumberOfDaysWithDate(42, 1234567890123L); when(newCodeDefStorage.read()).thenReturn(Optional.of(newCodeDefinition)); var ncd = underTest.getNewCodeDefinition("scope"); assertThat(ncd).extracting(GetNewCodeDefinitionResponse::getDescription, GetNewCodeDefinitionResponse::isSupported) .containsExactly("From last 42 days", true); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/nodejs/NodeJsHelperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.nodejs; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.mockito.stubbing.Answer; import org.sonar.api.utils.System2; import org.sonar.api.utils.command.Command; import org.sonar.api.utils.command.CommandExecutor; import org.sonar.api.utils.command.StreamConsumer; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class NodeJsHelperTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final Path DUMMY_FILE_HELPER_LOCATION = Paths.get(""); private static final Path FAKE_NODE_PATH = Paths.get("foo/node"); private final System2 system2 = mock(System2.class); private CommandExecutor commandExecutor; private final Map, BiFunction> registeredCommandAnswers = new LinkedHashMap<>(); @BeforeEach void prepare() { commandExecutor = mock(CommandExecutor.class); when(commandExecutor.execute(any(), any(), any(), anyLong())).thenAnswer((Answer) invocation -> { var c = invocation.getArgument(0, Command.class); for (Entry, BiFunction> answer : registeredCommandAnswers.entrySet()) { if (answer.getKey().test(c)) { var stdOut = invocation.getArgument(1, StreamConsumer.class); var stdErr = invocation.getArgument(2, StreamConsumer.class); return answer.getValue().apply(stdOut, stdErr); } } return fail("No answers registered for command: " + c.toString()); }); } @Test void usePropertyWhenProvidedToResolveNodePath() { registerNodeVersionAnswer("v10.5.4"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(FAKE_NODE_PATH); assertThat(logTester.logs()).containsExactly( "Node.js path provided by configuration: " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void supportNightlyBuilds() { registerNodeVersionAnswer("v15.0.0-nightly20200921039c274dde"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(FAKE_NODE_PATH); assertThat(logTester.logs()).containsExactly( "Node.js path provided by configuration: " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v15.0.0-nightly20200921039c274dde", "Detected node version: 15.0.0-nightly20200921039c274dde"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("15.0.0-nightly20200921039c274dde")); } @Test void ignoreCommandExecutionError() { registeredCommandAnswers.put(c -> true, (stdOut, stdErr) -> { stdErr.consumeLine("error"); return -1; }); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(FAKE_NODE_PATH); assertThat(logTester.logs()).containsExactly( "Node.js path provided by configuration: " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with -1\nstderr: error", "Unable to query node version"); assertThat(result).isNull(); } @Test void handleErrorDuringVersionCheck() { registerNodeVersionAnswer("wrong_version"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(FAKE_NODE_PATH); assertThat(logTester.logs()).containsExactly( "Node.js path provided by configuration: " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: wrong_version", "Unable to parse node version: wrong_version", "Unable to query node version"); assertThat(result).isNull(); } @Test void useWhichOnLinuxToResolveNodePath() { registerWhichAnswer(FAKE_NODE_PATH.toString()); registerNodeVersionAnswer("v10.5.4"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command '/usr/bin/which node'...", "Command '/usr/bin/which node' exited with 0\nstdout: " + FAKE_NODE_PATH, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void handleErrorDuringPathCheck() { registeredCommandAnswers.put(c -> true, (stdOut, stdErr) -> { stdErr.consumeLine("error"); return -1; }); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command '/usr/bin/which node'...", "Command '/usr/bin/which node' exited with -1\nstderr: error", "Unable to locate node"); assertThat(result).isNull(); } @Test void handleEmptyResponseDuringPathCheck() { when(system2.isOsWindows()).thenReturn(true); registerWhereAnswer(); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command 'C:\\Windows\\System32\\where.exe $PATH:node.exe'...", "Command 'C:\\Windows\\System32\\where.exe $PATH:node.exe' exited with 0", "Unable to locate node"); assertThat(result).isNull(); } @Test void useWhereOnWindowsToResolveNodePath() { when(system2.isOsWindows()).thenReturn(true); registerWhereAnswer(FAKE_NODE_PATH.toString()); registerNodeVersionAnswer("v10.5.4"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command 'C:\\Windows\\System32\\where.exe $PATH:node.exe'...", "Command 'C:\\Windows\\System32\\where.exe $PATH:node.exe' exited with 0\nstdout: " + FAKE_NODE_PATH, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } // SLCORE-281 @Test void whereOnWindowsCanReturnMultipleCandidates() { when(system2.isOsWindows()).thenReturn(true); var fake_node_path2 = Paths.get("foo2/node"); registerWhereAnswer(FAKE_NODE_PATH.toString(), fake_node_path2.toString()); registerNodeVersionAnswer("v10.5.4"); var underTest = new NodeJsHelper(system2, DUMMY_FILE_HELPER_LOCATION, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command 'C:\\Windows\\System32\\where.exe $PATH:node.exe'...", "Command 'C:\\Windows\\System32\\where.exe $PATH:node.exe' exited with 0\nstdout: " + FAKE_NODE_PATH + "\n" + fake_node_path2, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void usePathHelperOnMacToResolveNodePath(@TempDir Path tempDir) throws IOException { when(system2.isOsMac()).thenReturn(true); registerPathHelperAnswer("PATH=\"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/node\"; export PATH;"); registerWhichAnswerIfPathIsSet(FAKE_NODE_PATH.toString(), "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/node"); registerNodeVersionAnswer("v10.5.4"); // Need a true file since we are checking if file exists var fakePathHelper = tempDir.resolve("path_helper.sh"); Files.createFile(fakePathHelper); var underTest = new NodeJsHelper(system2, fakePathHelper, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command '" + fakePathHelper + " -s'...", "Command '" + fakePathHelper + " -s' exited with 0\nstdout: PATH=\"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/node\"; export PATH;", "Execute command '/usr/bin/which node'...", "Command '/usr/bin/which node' exited with 0\nstdout: " + FAKE_NODE_PATH, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void ignoreWrongPathHelperOutputOnMac(@TempDir Path tempDir) throws IOException { when(system2.isOsMac()).thenReturn(true); registerPathHelperAnswer("wrong \n output"); registerWhichAnswerIfPathIsSet(FAKE_NODE_PATH.toString(), System.getenv("PATH")); registerNodeVersionAnswer("v10.5.4"); // Need a true file since we are checking if file exists var fakePathHelper = tempDir.resolve("path_helper.sh"); Files.createFile(fakePathHelper); var underTest = new NodeJsHelper(system2, fakePathHelper, commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command '" + fakePathHelper + " -s'...", "Command '" + fakePathHelper + " -s' exited with 0\nstdout: wrong \n output", "Execute command '/usr/bin/which node'...", "Command '/usr/bin/which node' exited with 0\nstdout: " + FAKE_NODE_PATH, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void ignorePathHelperOnMacIfMissing() { when(system2.isOsMac()).thenReturn(true); registerPathHelperAnswer("wrong \n output"); registerWhichAnswerIfPathIsSet(FAKE_NODE_PATH.toString(), System.getenv("PATH")); registerNodeVersionAnswer("v10.5.4"); var underTest = new NodeJsHelper(system2, Paths.get("not_exists"), commandExecutor); var result = underTest.detect(null); assertThat(logTester.logs()).containsExactly( "Looking for node in the PATH", "Execute command '/usr/bin/which node'...", "Command '/usr/bin/which node' exited with 0\nstdout: " + FAKE_NODE_PATH, "Found node at " + FAKE_NODE_PATH, "Checking node version...", "Execute command '" + FAKE_NODE_PATH + " -v'...", "Command '" + FAKE_NODE_PATH + " -v' exited with 0\nstdout: v10.5.4", "Detected node version: 10.5.4"); assertThat(result).isNotNull(); assertThat(result.getPath()).isEqualTo(FAKE_NODE_PATH); assertThat(result.getVersion()).isEqualTo(Version.create("10.5.4")); } @Test void logWhenUnableToGetNodeVersion() { var underTest = new NodeJsHelper(); var result = underTest.detect(Paths.get("not_node")); assertThat(logTester.logs()).anyMatch(s -> s.startsWith("Unable to execute the command")); assertThat(result).isNull(); } private void registerNodeVersionAnswer(String version) { registeredCommandAnswers.put(c -> c.toString().endsWith(FAKE_NODE_PATH + " -v"), (stdOut, stdErr) -> { stdOut.consumeLine(version); return 0; }); } private void registerWhichAnswer(String whichOutput) { registeredCommandAnswers.put(c -> c.toString().endsWith("which node"), (stdOut, stdErr) -> { stdOut.consumeLine(whichOutput); return 0; }); } private void registerWhichAnswerIfPathIsSet(String whichOutput, @Nullable String expectedPath) { registeredCommandAnswers.put(c -> c.toString().endsWith("which node") && Objects.equals(expectedPath, c.getEnvironmentVariables().get("PATH")), (stdOut, stdErr) -> { stdOut.consumeLine(whichOutput); return 0; }); } private void registerWhereAnswer(String... whereOutput) { registeredCommandAnswers.put(c -> c.toString().endsWith("C:\\Windows\\System32\\where.exe $PATH:node.exe"), (stdOut, stdErr) -> { Stream.of(whereOutput).forEach(stdOut::consumeLine); return 0; }); } private void registerPathHelperAnswer(String output) { registeredCommandAnswers.put(c -> c.toString().endsWith("path_helper.sh -s"), (stdOut, stdErr) -> { stdOut.consumeLine(output); return 0; }); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/DotnetSupportTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Set; import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class DotnetSupportTest { private static final Path somePath = Paths.get("folder", "file.txt"); private InitializeParams initializeParams; static Stream provideTestArguments() { return Stream.of( Arguments.of(Language.CS, null, true, true), Arguments.of(Language.VBNET, null, true, true), Arguments.of(Language.CS, somePath, true, false), Arguments.of(Language.VBNET, somePath, true, false), Arguments.of(Language.CS, somePath, false, true), Arguments.of(Language.VBNET, somePath, false, true), Arguments.of(Language.CS, somePath, false, false), Arguments.of(Language.VBNET, somePath, false, false), Arguments.of(Language.COBOL, somePath, false, false) ); } @BeforeEach void prepare() { initializeParams = mock(InitializeParams.class); } @ParameterizedTest @MethodSource("provideTestArguments") void should_initialize_properties_as_expected(Language language, @Nullable Path csharpAnalyzerPath, boolean shouldUseCsharpEnterprise, boolean shouldUseVbNetEnterprise) { mockEnabledLanguages(language); var underTest = new DotnetSupport(initializeParams, csharpAnalyzerPath, shouldUseCsharpEnterprise, shouldUseVbNetEnterprise); assertThat(underTest.isSupportsCsharp()).isEqualTo(language == Language.CS); assertThat(underTest.isSupportsVbNet()).isEqualTo(language == Language.VBNET); assertThat(underTest.getActualCsharpAnalyzerPath()).isEqualTo(csharpAnalyzerPath); assertThat(underTest.isShouldUseCsharpEnterprise()).isEqualTo(shouldUseCsharpEnterprise); assertThat(underTest.isShouldUseVbNetEnterprise()).isEqualTo(shouldUseVbNetEnterprise); } private void mockEnabledLanguages(Language... languages) { when(initializeParams.getEnabledLanguagesInStandaloneMode()).thenReturn(Set.of(languages)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/PluginJarUtilsTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.nio.file.Files; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInfo; class PluginJarUtilsTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; @Test void should_return_null_when_jar_does_not_exist() { var missingJar = tempDir.resolve("missing.jar"); var version = PluginJarUtils.readVersion(missingJar); assertThat(version).isNull(); } @Test void should_return_null_when_jar_is_invalid() throws IOException { var corruptedJar = tempDir.resolve("corrupted.jar"); Files.writeString(corruptedJar, "not a jar"); var version = PluginJarUtils.readVersion(corruptedJar); assertThat(version).isNull(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/PluginLifecycleServiceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import java.util.Map; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PluginLifecycleServiceTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private PluginsService pluginsService; private RulesRepository rulesRepository; private ActiveRulesService activeRulesService; private PluginLifecycleService underTest; @BeforeEach void setUp() { pluginsService = mock(PluginsService.class); rulesRepository = mock(RulesRepository.class); activeRulesService = mock(ActiveRulesService.class); underTest = new PluginLifecycleService(pluginsService, rulesRepository, activeRulesService); } @Test void unloadPluginsAndEvictCaches_delegates_to_all_three_services() { var connectionId = "conn1"; underTest.unloadPluginsAndEvictCaches(connectionId); verify(pluginsService).unloadPlugins(connectionId); verify(rulesRepository).evictFor(connectionId); verify(activeRulesService).evictFor(connectionId); } @Test void reloadPluginsAndEvictCaches_unloads_then_loads_and_returns_new_plugins() { var connectionId = "conn1"; var loadedPlugins = mock(LoadedPlugins.class); when(pluginsService.getPlugins(connectionId)).thenReturn(new PluginsConfiguration(null, loadedPlugins, Map.of())); var result = underTest.reloadPluginsAndEvictCaches(connectionId); verify(pluginsService).unloadPlugins(connectionId); verify(rulesRepository).evictFor(connectionId); verify(activeRulesService).evictFor(connectionId); verify(pluginsService).getPlugins(connectionId); assertThat(result.plugins()).isSameAs(loadedPlugins); } @Test void unloadEmbeddedPluginsAndEvictCaches_delegates_to_all_three_services() { underTest.unloadEmbeddedPluginsAndEvictCaches(); verify(pluginsService).unloadEmbeddedPlugins(); verify(rulesRepository).evictEmbedded(); verify(activeRulesService).evictStandalone(); } @Test void reloadEmbeddedPluginsAndEvictCaches_unloads_then_loads_and_returns_new_embedded_plugins() { var loadedPlugins = mock(LoadedPlugins.class); when(pluginsService.getEmbeddedPlugins()).thenReturn(new PluginsConfiguration(null, loadedPlugins, Map.of())); var result = underTest.reloadEmbeddedPluginsAndEvictCaches(); verify(pluginsService).unloadEmbeddedPlugins(); verify(rulesRepository).evictEmbedded(); verify(activeRulesService).evictStandalone(); verify(pluginsService).getEmbeddedPlugins(); assertThat(result.plugins()).isSameAs(loadedPlugins); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/PluginStatusNotifierServiceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.repository.config.BindingConfiguration; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.config.ConfigurationScope; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.ArtifactSourceDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStateDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidChangePluginStatusesParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PluginStatusNotifierServiceTest { private static final String SCOPE_1 = "scope1"; private static final String SCOPE_2 = "scope2"; private static final String CONNECTION_1 = "conn1"; private final SonarLintRpcClient client = mock(SonarLintRpcClient.class); private final PluginsService pluginsService = mock(PluginsService.class); private final ConfigurationRepository configurationRepository = new ConfigurationRepository(); private final PluginStatusNotifierService underTest = new PluginStatusNotifierService(pluginsService, client, configurationRepository); private final PluginStatus standaloneStatus = PluginStatus.forLanguage(SonarLanguage.JAVA, ArtifactState.ACTIVE, ArtifactOrigin.EMBEDDED, null, null, null, null); private final PluginStatus connectedStatus = PluginStatus.forLanguage(SonarLanguage.JAVA, ArtifactState.ACTIVE, ArtifactOrigin.SONARQUBE_SERVER, null, null, null, "10.1"); @BeforeEach void setUp() { when(pluginsService.getPluginStatuses(null)).thenReturn(List.of(standaloneStatus)); when(pluginsService.getPluginStatuses(CONNECTION_1)).thenReturn(List.of(connectedStatus)); } @Test void should_notify_each_scope_with_its_effective_connection_statuses_in_standalone_mode() { configurationRepository.addOrReplace(new ConfigurationScope(SCOPE_1, null, true, "Scope 1"), BindingConfiguration.noBinding()); configurationRepository.addOrReplace(new ConfigurationScope(SCOPE_2, null, true, "Scope 2"), new BindingConfiguration(CONNECTION_1, "project1", false)); var expectedScope1Params = new DidChangePluginStatusesParams(SCOPE_1, List.of(standaloneStatusDto())); var expectedScope2Params = new DidChangePluginStatusesParams(SCOPE_2, List.of(connectedStatusDto())); underTest.onPluginStatusesChanged(new PluginStatusesChangedEvent(null, List.of())); var captor = ArgumentCaptor.forClass(DidChangePluginStatusesParams.class); verify(client, times(2)).didChangePluginStatuses(captor.capture()); assertThat(captor.getAllValues()) .usingRecursiveComparison() .ignoringCollectionOrder() .isEqualTo(List.of(expectedScope1Params, expectedScope2Params)); } @Test void should_notify_only_bound_scopes_in_connected_mode() { configurationRepository.addOrReplace(new ConfigurationScope(SCOPE_1, null, true, "Scope 1"), new BindingConfiguration(CONNECTION_1, "project1", false)); configurationRepository.addOrReplace(new ConfigurationScope(SCOPE_2, null, true, "Scope 2"), new BindingConfiguration(CONNECTION_1, "project2", false)); configurationRepository.addOrReplace(new ConfigurationScope("scope3", null, true, "Scope 3"), BindingConfiguration.noBinding()); var expectedScope1Params = new DidChangePluginStatusesParams(SCOPE_1, List.of(connectedStatusDto())); var expectedScope2Params = new DidChangePluginStatusesParams(SCOPE_2, List.of(connectedStatusDto())); underTest.onPluginStatusesChanged(new PluginStatusesChangedEvent(CONNECTION_1, List.of(connectedStatus))); var captor = ArgumentCaptor.forClass(DidChangePluginStatusesParams.class); verify(client, times(2)).didChangePluginStatuses(captor.capture()); assertThat(captor.getAllValues()) .usingRecursiveComparison() .ignoringCollectionOrder() .isEqualTo(List.of(expectedScope1Params, expectedScope2Params)); } private static PluginStatusDto standaloneStatusDto() { return new PluginStatusDto(Language.JAVA, "Java", PluginStateDto.ACTIVE, ArtifactSourceDto.EMBEDDED, null, null, null); } private static PluginStatusDto connectedStatusDto() { return new PluginStatusDto(Language.JAVA, "Java", PluginStateDto.ACTIVE, ArtifactSourceDto.SONARQUBE_SERVER, null, null, "10.1"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/PluginsServiceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ArtifactsLoadingResult; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ConnectedArtifactsLoadingStrategy; import org.sonarsource.sonarlint.core.plugin.loading.strategy.ConnectedArtifactsLoadingStrategyFactory; import org.sonarsource.sonarlint.core.plugin.loading.strategy.StandaloneArtifactsLoadingStrategy; import org.sonarsource.sonarlint.core.plugin.skipped.SkippedPluginsRepository; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.repository.connection.AbstractConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class PluginsServiceTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final Path ossPath = Paths.get("folder", "oss"); private static final Path enterprisePath = Paths.get("folder", "enterprise"); private PluginsService underTest; private PluginsRepository pluginsRepository; private ConnectionConfigurationRepository connectionConfigurationStorage; private StorageService storageService; private ConnectionStorage connectionStorage; private ServerInfoStorage serverInfoStorage; private PluginsStorage pluginStorage; private InitializeParams initializeParams; private ApplicationEventPublisher eventPublisher; private ConnectedArtifactsLoadingStrategyFactory connectedArtifactsLoadingStrategyFactory; @BeforeEach void prepare() { pluginsRepository = mock(PluginsRepository.class); storageService = mock(StorageService.class); connectionConfigurationStorage = mock(ConnectionConfigurationRepository.class); connectionStorage = mock(ConnectionStorage.class); serverInfoStorage = mock(ServerInfoStorage.class); pluginStorage = mock(PluginsStorage.class); when(connectionStorage.plugins()).thenReturn(pluginStorage); initializeParams = mock(InitializeParams.class); when(initializeParams.getDisabledPluginKeysForAnalysis()).thenReturn(Set.of()); eventPublisher = mock(ApplicationEventPublisher.class); when(pluginStorage.getStoredPluginsByKey()).thenReturn(Map.of()); var standaloneArtifactsLoadingStrategy = mock(StandaloneArtifactsLoadingStrategy.class); connectedArtifactsLoadingStrategyFactory = mock(ConnectedArtifactsLoadingStrategyFactory.class); var connectedArtifactsLoadingStrategy = mock(ConnectedArtifactsLoadingStrategy.class); var csharpArtifact = new ResolvedArtifact(ArtifactState.ACTIVE, ossPath, ArtifactOrigin.EMBEDDED, null, null); when(standaloneArtifactsLoadingStrategy.resolveArtifacts()).thenReturn(new ArtifactsLoadingResult(Set.of(), Map.of("csharp", csharpArtifact))); when(connectedArtifactsLoadingStrategy.resolveArtifacts()).thenReturn(new ArtifactsLoadingResult(Set.of(), Map.of("csharp", csharpArtifact))); when(connectedArtifactsLoadingStrategyFactory.getOrCreate(any())).thenReturn(connectedArtifactsLoadingStrategy); var binariesArtifactSource = mock(BinariesArtifactSource.class); when(binariesArtifactSource.getOmnisharpExtraProperties()).thenReturn(Map.of()); underTest = new PluginsService(pluginsRepository, mock(SkippedPluginsRepository.class), storageService, initializeParams, connectionConfigurationStorage, mock(NodeJsService.class), eventPublisher, standaloneArtifactsLoadingStrategy, connectedArtifactsLoadingStrategyFactory, binariesArtifactSource); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionDoesNotExist_returnsFalse() { var connectionId = "notExisting"; mockNoConnection(connectionId); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionIsToCloud_returnsTrue() { var connectionId = "SQC"; var connection = createConnection(connectionId, ConnectionKind.SONARCLOUD); mockConnection(connection); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isTrue(); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionIsToServerThatDoesNotExistOnStorage_returnsFalse() { var connectionId = "SQS"; var connection = createConnection(connectionId, ConnectionKind.SONARQUBE); mockConnection(connection); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionIsToServer_Older_Than_10_8_returnsTrue() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.7")); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isTrue(); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionIsToServerWithRepackagedPluginAndPluginIsNotPresentOnTheServer_returnsFalse() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin("otherPlugin"); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseCSharpAnalyzer_connectionIsToServerWithRepackagedPluginAndPluginIsPresentOnTheServer_returnsTrue() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin(PluginsService.CSHARP_ENTERPRISE_PLUGIN_ID); var result = underTest.shouldUseEnterpriseCSharpAnalyzer(connectionId); assertThat(result).isTrue(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionDoesNotExist_returnsFalse() { var connectionId = "notExisting"; mockNoConnection(connectionId); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionIsToCloud_returnsTrue() { var connectionId = "SQC"; var connection = createConnection(connectionId, ConnectionKind.SONARCLOUD); mockConnection(connection); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isTrue(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionIsToServerThatDoesNotExistOnStorage_returnsFalse() { var connectionId = "SQS"; var connection = createConnection(connectionId, ConnectionKind.SONARQUBE); mockConnection(connection); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionIsToServer_Older_Than_10_8_returnsTrue() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.7")); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isTrue(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionIsToServerWithRepackagedPluginAndPluginIsNotPresentOnTheServer_returnsFalse() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin("otherPlugin"); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isFalse(); } @Test void shouldUseEnterpriseVbAnalyzer_connectionIsToServerWithRepackagedPluginAndPluginIsPresentOnTheServer_returnsTrue() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin(PluginsService.VBNET_ENTERPRISE_PLUGIN_ID); var result = underTest.shouldUseEnterpriseVbAnalyzer(connectionId); assertThat(result).isTrue(); } @ParameterizedTest @EnumSource(value = Language.class, names = {"CS", "VBNET", "COBOL"}) void getEmbeddedPlugins_extraProperties_ReturnsExpectedDotnetProperties(Language language) { mockEnabledLanguages(language); var props = underTest.getEmbeddedPlugins().extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", ossPath.toString()); if (language == Language.CS) { assertThat(props).containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "false"); } if (language == Language.VBNET) { assertThat(props).containsEntry("sonar.cs.internal.shouldUseVbEnterprise", "false"); } } @Test void getPlugins_extraProperties_forCloud_fallsBackToOss_whenEnterpriseNotInStorage() { var connectionId = "SQC"; var connection = createConnection(connectionId, ConnectionKind.SONARCLOUD); mockConnection(connection); mockEnabledLanguages(Language.CS); var props = underTest.getPlugins(connectionId).extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", ossPath.toString()) .containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "true"); } @Test void getPlugins_extraProperties_forCloud_ReturnsEnterpriseProperties() { var connectionId = "SQC"; var connection = createConnection(connectionId, ConnectionKind.SONARCLOUD); mockConnection(connection); mockPlugin(PluginsService.CSHARP_ENTERPRISE_PLUGIN_ID, enterprisePath); mockEnabledLanguages(Language.CS, Language.VBNET); var props = underTest.getPlugins(connectionId).extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", enterprisePath.toString()) .containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "true") .containsEntry("sonar.cs.internal.shouldUseVbEnterprise", "true"); } @Test void getPlugins_extraProperties_connectionIsToServer_Older_Than_10_8_ReturnsEnterpriseProperties() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.7")); mockPlugin(PluginsService.CSHARP_ENTERPRISE_PLUGIN_ID, enterprisePath); mockEnabledLanguages(Language.CS, Language.VBNET); var props = underTest.getPlugins(connectionId).extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", enterprisePath.toString()) .containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "true") .containsEntry("sonar.cs.internal.shouldUseVbEnterprise", "true"); } @Test void getPlugins_extraProperties_connectionIsToServerWithRepackagedCsharpPlugin_ReturnsEnterprisePropertiesForCsharp() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin(PluginsService.CSHARP_ENTERPRISE_PLUGIN_ID, enterprisePath); mockEnabledLanguages(Language.CS, Language.VBNET); var props = underTest.getPlugins(connectionId).extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", enterprisePath.toString()) .containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "true") .containsEntry("sonar.cs.internal.shouldUseVbEnterprise", "false"); } @Test void getPlugins_extraProperties_connectionIsToServerWithRepackagedVbPlugin_ReturnsEnterprisePropertiesForVb() { var connectionId = "SQS"; mockConnection(connectionId, ConnectionKind.SONARQUBE, Version.create("10.8")); mockPlugin(PluginsService.VBNET_ENTERPRISE_PLUGIN_ID); mockEnabledLanguages(Language.CS, Language.VBNET); var props = underTest.getPlugins(connectionId).extraProperties(); assertThat(props) .containsEntry("sonar.cs.internal.analyzerPath", ossPath.toString()) .containsEntry("sonar.cs.internal.shouldUseCsharpEnterprise", "false") .containsEntry("sonar.cs.internal.shouldUseVbEnterprise", "true"); } @Test void should_return_list_size_equal_to_sonar_language_values() { var connectionId = "connection1"; mockNoConnection(connectionId); var result = underTest.getPluginStatuses(connectionId); assertThat(result).hasSize(SonarLanguage.values().length); } @Test void unloadPlugins_should_not_publish_event_when_no_plugins_were_loaded() { var connectionId = "connection1"; underTest.unloadPlugins(connectionId); verify(eventPublisher, never()).publishEvent(any()); } @Test void unloadPlugins_should_evict_connected_strategy_from_cache() { var connectionId = "connection1"; underTest.unloadPlugins(connectionId); verify(connectedArtifactsLoadingStrategyFactory).evict(connectionId); } @Test void unloadEmbeddedPlugins_should_not_publish_event_when_no_embedded_plugins_were_loaded() { underTest.unloadPlugins(null); verify(eventPublisher, never()).publishEvent(any()); } private void mockNoConnection(String connectionId) { when(connectionStorage.serverInfo()).thenReturn(serverInfoStorage); when(storageService.connection(connectionId)).thenReturn(connectionStorage); when(connectionConfigurationStorage.getConnectionById(connectionId)).thenReturn(null); } private void mockConnection(String connectionId, ConnectionKind kind, Version version) { var connection = createConnection(connectionId, kind); mockConnection(connection); mockConnectionVersion(version); } private AbstractConnectionConfiguration createConnection(String connectionId, ConnectionKind kind) { var connection = mock(AbstractConnectionConfiguration.class); when(connection.getConnectionId()).thenReturn(connectionId); when(connection.getKind()).thenReturn(kind); return connection; } private void mockConnection(AbstractConnectionConfiguration connection) { when(connectionStorage.serverInfo()).thenReturn(serverInfoStorage); when(storageService.connection(connection.getConnectionId())).thenReturn(connectionStorage); when(connectionConfigurationStorage.getConnectionById(connection.getConnectionId())).thenReturn(connection); } private void mockPlugin(String pluginKey) { mockPlugin(pluginKey, null); } private void mockPlugin(String pluginKey, @Nullable Path jarPath) { var plugin = mock(StoredPlugin.class); when(plugin.getKey()).thenReturn(pluginKey); when(plugin.getJarPath()).thenReturn(jarPath); when(pluginStorage.getStoredPlugins()).thenReturn(List.of(plugin)); when(pluginStorage.getStoredPluginsByKey()).thenReturn(Map.of(pluginKey, plugin)); } private void mockConnectionVersion(Version version) { var serverInfo = mock(StoredServerInfo.class); when(serverInfo.version()).thenReturn(version); when(serverInfoStorage.read()).thenReturn(Optional.of(serverInfo)); } private void mockEnabledLanguages(Language... languages) { when(initializeParams.getEnabledLanguagesInStandaloneMode()).thenReturn(Set.of(languages)); when(initializeParams.getBackendCapabilities()).thenReturn(Set.of()); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/ConnectedArtifactsLoadingStrategyTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.nio.file.Path; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.plugin.source.server.ServerPluginSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class ConnectedArtifactsLoadingStrategyTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private ServerPluginSource serverSource; private BinariesArtifactSource binariesSource; private LanguageSupportRepository languageSupportRepository; private InitializeParams params; @BeforeEach void setUp() { serverSource = mock(ServerPluginSource.class); binariesSource = mock(BinariesArtifactSource.class); languageSupportRepository = mock(LanguageSupportRepository.class); params = mock(InitializeParams.class); when(params.getConnectedModeEmbeddedPluginPathsByKey()).thenReturn(Map.of()); when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of()); when(serverSource.load(any())).thenAnswer(inv -> { @SuppressWarnings("unchecked") var keys = (Set) inv.getArgument(0); return new LoadResult(keys.stream().collect(Collectors.toMap(k -> k, k -> new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null)))); }); when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of()); when(binariesSource.load(any())).thenAnswer(inv -> { @SuppressWarnings("unchecked") var keys = (Set) inv.getArgument(0); return new LoadResult(keys.stream().collect(Collectors.toMap(k -> k, k -> new ResolvedArtifact(ArtifactState.ACTIVE, null, ArtifactOrigin.ON_DEMAND, null, null)))); }); when(languageSupportRepository.getEnabledLanguagesInConnectedMode()).thenReturn(EnumSet.noneOf(SonarLanguage.class)); } // --- Server plugin included --- @Test void resolvePlugins_should_include_java_from_server_when_listed_as_available() { when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of(new AvailableArtifact(SonarPlugin.JAVA.getKey(), null, false, Optional.empty()))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()) .containsEntry(SonarPlugin.JAVA.getKey(), new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null)); } // --- Binary fallback --- @Test void resolvePlugins_should_fall_back_to_binaries_when_server_does_not_list_language_plugin() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of(new AvailableArtifact(SonarPlugin.JAVA.getKey(), null, false, Optional.empty()))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()) .containsEntry(SonarPlugin.JAVA.getKey(), new ResolvedArtifact(ArtifactState.ACTIVE, null, ArtifactOrigin.ON_DEMAND, null, null)); } @Test void resolvePlugins_should_not_include_plugin_not_listed_by_any_source() { var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPlugin.COBOL.getKey()); } // --- Enterprise deduplication (different-key variants: CS, VBNET) --- @Test void resolvePlugins_should_remove_base_key_when_enterprise_variant_is_present() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of(new AvailableArtifact(SonarPlugin.CS_OSS.getKey(), null, false, Optional.empty()))); when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of(new AvailableArtifact(SonarPlugin.CSHARP_ENTERPRISE.getKey(), null, true, Optional.empty()))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()) .containsKey(SonarPlugin.CSHARP_ENTERPRISE.getKey()) .doesNotContainKey(SonarPlugin.CS_OSS.getKey()); // binaries source is called with an empty set (pre-populate), but never with actual plugin keys verify(binariesSource).load(Set.of()); } // --- Enterprise priority override (same-key variants: GO, IAC, TEXT) --- @Test void resolvePlugins_should_prefer_server_enterprise_over_embedded_for_same_key_plugins() { // Embedded has "go" (higher normal priority than server) when(params.getConnectedModeEmbeddedPluginPathsByKey()).thenReturn(Map.of(SonarPlugin.GO.getKey(), Path.of("go-embedded.jar"))); // Server also has "go", flagged as enterprise when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of(new AvailableArtifact(SonarPlugin.GO.getKey(), null, true, Optional.empty()))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); // Enterprise server must win over embedded assertThat(result.resolvedArtifactsByKey()) .containsEntry(SonarPlugin.GO.getKey(), new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null)); verify(serverSource).load(Set.of(SonarPlugin.GO.getKey())); } // --- Dependency removal (no dependent available) --- @Test void resolvePlugins_should_remove_dependency_when_dependent_plugin_is_not_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPluginDependency.OMNISHARP_MONO.getKey()); } // --- Plugin removal when required dependency is missing --- @Test void resolvePlugins_should_remove_plugin_when_a_required_dependency_is_not_available() { when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPlugin.SONARLINT_OMNISHARP.getKey()); } @Test void resolvePlugins_should_keep_plugin_when_all_required_dependencies_are_available() { when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)), new AvailableArtifact(SonarPlugin.CS_OSS.getKey(), null, false, Optional.of(SonarPlugin.CS_OSS)))); when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET472.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET472)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET6.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET6)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).containsKey(SonarPlugin.SONARLINT_OMNISHARP.getKey()); } @Test void resolvePlugins_should_keep_dependency_when_dependent_plugin_is_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)))); when(serverSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).containsKey(SonarPluginDependency.OMNISHARP_MONO.getKey()); } private ConnectedArtifactsLoadingStrategy createStrategy() { return new ConnectedArtifactsLoadingStrategy(params, binariesSource, serverSource, languageSupportRepository); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/loading/strategy/StandaloneArtifactsLoadingStrategyTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.loading.strategy; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.commons.plugins.SonarPluginDependency; import org.sonarsource.sonarlint.core.languages.LanguageSupportRepository; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifactSource; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class StandaloneArtifactsLoadingStrategyTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private BinariesArtifactSource binariesSource; private LanguageSupportRepository languageSupportRepository; private InitializeParams params; @BeforeEach void setUp() { binariesSource = mock(BinariesArtifactSource.class); languageSupportRepository = mock(LanguageSupportRepository.class); params = mock(InitializeParams.class); when(params.getEmbeddedPluginPaths()).thenReturn(java.util.Set.of()); when(params.getConnectedModeEmbeddedPluginPathsByKey()).thenReturn(Map.of()); when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of()); when(binariesSource.load(any())).thenAnswer(inv -> { @SuppressWarnings("unchecked") var keys = (Set) inv.getArgument(0); return new LoadResult(keys.stream().collect(Collectors.toMap(k -> k, k -> new ResolvedArtifact(ArtifactState.ACTIVE, null, ArtifactOrigin.ON_DEMAND, null, null)))); }); when(languageSupportRepository.getEnabledLanguagesInStandaloneMode()).thenReturn(EnumSet.noneOf(SonarLanguage.class)); when(languageSupportRepository.isEnabledOnlyInConnectedMode(any())).thenReturn(false); } // --- Enterprise deduplication (different-key variants: CS, VBNET) --- @Test void resolvePlugins_should_remove_base_key_when_enterprise_variant_is_present() { // Both the OSS base and the enterprise variant are available (e.g. base from binaries, // enterprise from embedded). Only the enterprise variant should survive. when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.CS_OSS.getKey(), null, false, Optional.of(SonarPlugin.CS_OSS)), new AvailableArtifact(SonarPlugin.CSHARP_ENTERPRISE.getKey(), null, true, Optional.of(SonarPlugin.CSHARP_ENTERPRISE)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()) .containsKey(SonarPlugin.CSHARP_ENTERPRISE.getKey()) .doesNotContainKey(SonarPlugin.CS_OSS.getKey()); // binaries must not be asked to load the non-enterprise key verify(binariesSource).load(Set.of(SonarPlugin.CSHARP_ENTERPRISE.getKey())); } // --- Dependency removal (no dependent available) --- @Test void resolvePlugins_should_remove_dependency_when_dependent_plugin_is_not_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPluginDependency.OMNISHARP_MONO.getKey()); } @Test void resolvePlugins_should_keep_dependency_when_dependent_plugin_is_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)), new AvailableArtifact(SonarPlugin.CS_OSS.getKey(), null, false, Optional.of(SonarPlugin.CS_OSS)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET472.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET472)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET6.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET6)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).containsKey(SonarPluginDependency.OMNISHARP_MONO.getKey()); } // --- Plugin removal when required dependency is missing --- @Test void resolvePlugins_should_remove_plugin_when_a_required_dependency_is_not_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPlugin.SONARLINT_OMNISHARP.getKey()); } @Test void resolvePlugins_should_keep_plugin_when_all_required_dependencies_are_available() { when(binariesSource.listAvailableArtifacts(any())).thenReturn(List.of( new AvailableArtifact(SonarPlugin.SONARLINT_OMNISHARP.getKey(), null, false, Optional.of(SonarPlugin.SONARLINT_OMNISHARP)), new AvailableArtifact(SonarPlugin.CS_OSS.getKey(), null, false, Optional.of(SonarPlugin.CS_OSS)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_MONO.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_MONO)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET472.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET472)), new AvailableArtifact(SonarPluginDependency.OMNISHARP_NET6.getKey(), null, false, Optional.of(SonarPluginDependency.OMNISHARP_NET6)))); var strategy = createStrategy(); var result = strategy.resolveArtifacts(); assertThat(result.resolvedArtifactsByKey()).containsKey(SonarPlugin.SONARLINT_OMNISHARP.getKey()); } private StandaloneArtifactsLoadingStrategy createStrategy() { return new StandaloneArtifactsLoadingStrategy(params, binariesSource, languageSupportRepository); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/BinariesArtifactTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.sonarsource.sonarlint.core.plugin.source.binaries.BinariesArtifact; import uk.org.webcompere.systemstubs.jupiter.SystemStub; import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; import uk.org.webcompere.systemstubs.properties.SystemProperties; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SystemStubsExtension.class) class BinariesArtifactTest { @SystemStub SystemProperties systemProperties; @Test void should_find_artifact_by_key() { var actual = BinariesArtifact.findByKey("cpp"); assertThat(actual).contains(BinariesArtifact.CFAMILY_PLUGIN); } @Test void should_return_empty_for_unknown_key() { var actual = BinariesArtifact.findByKey("unknown"); assertThat(actual).isEmpty(); } @Test void should_return_empty_for_null_key() { var actual = BinariesArtifact.findByKey(null); assertThat(actual).isEmpty(); } @Test void should_return_version_from_properties() { var actual = BinariesArtifact.CFAMILY_PLUGIN.version(); assertThat(actual).isEqualTo("6.80.0.98490"); } @Test void should_use_default_base_url() { var actual = BinariesArtifact.CFAMILY_PLUGIN.urlPattern(); assertThat(actual).isEqualTo("https://binaries.sonarsource.com/CommercialDistribution/sonar-cfamily-plugin/sonar-cfamily-plugin-%s.jar"); } @Test void should_use_overridden_base_url_when_system_property_set() { systemProperties.set(BinariesArtifact.PROPERTY_URL_PATTERN, "http://mock-server"); var actual = BinariesArtifact.CFAMILY_PLUGIN.urlPattern(); assertThat(actual).isEqualTo("http://mock-server/CommercialDistribution/sonar-cfamily-plugin/sonar-cfamily-plugin-%s.jar"); } @Test void should_return_artifact_key() { assertThat(BinariesArtifact.CFAMILY_PLUGIN.artifactKey()).isEqualTo("cpp"); } @Test void should_return_correct_signature_resource_paths() { assertThat(BinariesArtifact.CFAMILY_PLUGIN.signatureResourcePath()).isEqualTo("ondemand/sonar-cpp-plugin.jar.asc"); assertThat(BinariesArtifact.CSHARP_OSS.signatureResourcePath()).isEqualTo("ondemand/sonar-cs-plugin.jar.asc"); assertThat(BinariesArtifact.OMNISHARP_MONO.signatureResourcePath()).isEqualTo("ondemand/omnisharp-mono.tar.gz.asc"); assertThat(BinariesArtifact.OMNISHARP_NET472.signatureResourcePath()).isEqualTo("ondemand/omnisharp-net472.tar.gz.asc"); assertThat(BinariesArtifact.OMNISHARP_NET6.signatureResourcePath()).isEqualTo("ondemand/omnisharp-net6.0.tar.gz.asc"); } @Test void should_return_omnisharp_version_from_properties() { assertThat(BinariesArtifact.OMNISHARP_MONO.version()).isEqualTo("1.39.15"); assertThat(BinariesArtifact.OMNISHARP_NET472.version()).isEqualTo("1.39.15"); assertThat(BinariesArtifact.OMNISHARP_NET6.version()).isEqualTo("1.39.15"); } @Test void should_return_correct_omnisharp_url_patterns() { assertThat(BinariesArtifact.OMNISHARP_MONO.urlPattern()) .isEqualTo("https://binaries.sonarsource.com/OmniSharp-Roslyn/%s/omnisharp-mono.tar.gz"); assertThat(BinariesArtifact.OMNISHARP_NET472.urlPattern()) .isEqualTo("https://binaries.sonarsource.com/OmniSharp-Roslyn/%s/omnisharp-net472.tar.gz"); assertThat(BinariesArtifact.OMNISHARP_NET6.urlPattern()) .isEqualTo("https://binaries.sonarsource.com/OmniSharp-Roslyn/%s/omnisharp-net6.0.tar.gz"); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesArtifactSourceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.event.PluginStatusUpdateEvent; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.plugin.PluginStatus; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class BinariesArtifactSourceTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private HttpClientProvider httpClientProvider; private ApplicationEventPublisher eventPublisher; private List capturedStatuses; private BinariesSignatureVerifier signatureVerifier; private BinariesLocalCacheManager cacheManager; @BeforeEach void setUp() { httpClientProvider = mock(HttpClientProvider.class); eventPublisher = mock(ApplicationEventPublisher.class); signatureVerifier = mock(BinariesSignatureVerifier.class); cacheManager = mock(BinariesLocalCacheManager.class); capturedStatuses = new CopyOnWriteArrayList<>(); doAnswer(inv -> { capturedStatuses.addAll(inv.getArgument(0, PluginStatusUpdateEvent.class).newStatuses()); return null; }).when(eventPublisher).publishEvent(any(PluginStatusUpdateEvent.class)); } @Test void load_should_return_empty_when_plugin_key_not_handled() { var source = buildSource(); var result = source.load(Set.of("java")); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey("java"); } @Test void load_should_return_downloading_on_first_async_call_for_cfamily() { var proceedLatch = new CountDownLatch(1); mockBlockingHttpClient(proceedLatch); var source = buildSource(); try { var result = source.load(Set.of("cpp")); assertThat(result.resolvedArtifactsByKey().get("cpp")) .usingRecursiveComparison() .ignoringFields("downloadFuture") .isEqualTo(downloading()); } finally { proceedLatch.countDown(); await().atMost(5, TimeUnit.SECONDS).until(() -> !capturedStatuses.isEmpty()); } } @Test void load_should_return_downloading_while_same_artifact_is_in_progress() { var proceedLatch = new CountDownLatch(1); mockBlockingHttpClient(proceedLatch); var source = buildSource(); try { source.load(Set.of("cpp")); var result = source.load(Set.of("cpp")); assertThat(result.resolvedArtifactsByKey().get("cpp")) .usingRecursiveComparison() .ignoringFields("downloadFuture") .isEqualTo(downloading()); } finally { proceedLatch.countDown(); await().atMost(5, TimeUnit.SECONDS).until(() -> !capturedStatuses.isEmpty()); } } @Test void load_should_fire_failed_event_on_async_download_error() { var httpClient = mock(HttpClient.class); when(httpClient.get(anyString())).thenThrow(new RuntimeException("Connection refused")); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); var source = buildSource(); source.load(Set.of("cpp")); await().atMost(5, TimeUnit.SECONDS).until(() -> capturedStatuses.size() == 3); assertThat(capturedStatuses).containsExactlyInAnyOrder( failedStatus(SonarLanguage.C), failedStatus(SonarLanguage.CPP), failedStatus(SonarLanguage.OBJC)); } @Test void load_should_fire_failed_event_when_signature_verification_fails() throws Exception { mockSuccessfulHttpClient(); when(signatureVerifier.verify(any(Path.class), any(BinariesArtifact.class))).thenReturn(false); var source = buildSource(); source.load(Set.of("cpp")); await().atMost(5, TimeUnit.SECONDS).until(() -> capturedStatuses.size() == 3); assertThat(capturedStatuses).containsExactlyInAnyOrder( failedStatus(SonarLanguage.C), failedStatus(SonarLanguage.CPP), failedStatus(SonarLanguage.OBJC)); } @Test void load_should_fire_active_event_covering_all_languages_on_successful_async_download() throws Exception { mockSuccessfulHttpClient(); when(signatureVerifier.verify(any(Path.class), any(BinariesArtifact.class))).thenReturn(true); var source = buildSource(); source.load(Set.of("cpp")); await().atMost(10, TimeUnit.SECONDS).until(() -> capturedStatuses.size() == 3); var artifactVersion = BinariesArtifact.CFAMILY_PLUGIN.version(); var pluginPath = tempDir.resolve("ondemand-plugins").resolve("cpp").resolve(artifactVersion) .resolve("sonar-cpp-plugin-" + artifactVersion + ".jar"); assertThat(capturedStatuses).containsExactlyInAnyOrder( activeStatus(SonarLanguage.C, pluginPath), activeStatus(SonarLanguage.CPP, pluginPath), activeStatus(SonarLanguage.OBJC, pluginPath)); // cleanupOldVersions must receive the artifact-key directory (.../ondemand-plugins/cpp/), // not the version directory or the JAR's parent verify(cacheManager).cleanupOldVersions( tempDir.resolve("ondemand-plugins").resolve("cpp"), artifactVersion); } @Test void load_should_return_active_on_warm_startup_when_omnisharp_directory_exists_and_is_non_empty() throws Exception { var source = buildSource(); var artifactVersion = BinariesArtifact.OMNISHARP_MONO.version(); var omnisharpDir = tempDir.resolve("ondemand-plugins").resolve("omnisharp-mono").resolve(artifactVersion); Files.createDirectories(omnisharpDir); Files.createFile(omnisharpDir.resolve("OmniSharp.exe")); var result = source.load(Set.of("omnisharp-mono")); assertThat(result.resolvedArtifactsByKey().get("omnisharp-mono")) .usingRecursiveComparison() .ignoringFields("downloadFuture") .isEqualTo(new ResolvedArtifact(ArtifactState.ACTIVE, omnisharpDir, ArtifactOrigin.ON_DEMAND, Version.create(artifactVersion), null)); } @Test void load_should_re_download_when_omnisharp_directory_is_empty() throws Exception { var proceedLatch = new CountDownLatch(1); mockBlockingHttpClient(proceedLatch); var source = buildSource(); var artifactVersion = BinariesArtifact.OMNISHARP_MONO.version(); var omnisharpDir = tempDir.resolve("ondemand-plugins").resolve("omnisharp-mono").resolve(artifactVersion); Files.createDirectories(omnisharpDir); try { var result = source.load(Set.of("omnisharp-mono")); assertThat(result.resolvedArtifactsByKey().get("omnisharp-mono")) .usingRecursiveComparison() .ignoringFields("downloadFuture") .isEqualTo(downloading()); } finally { proceedLatch.countDown(); await().atMost(5, TimeUnit.SECONDS).until(() -> !capturedStatuses.isEmpty()); } } @Test void list_AvailablePlugins_should_return_entries_for_all_plugins() throws Exception { mockSuccessfulHttpClient(); when(signatureVerifier.verify(any(Path.class), any(BinariesArtifact.class))).thenReturn(true); var source = buildSource(); var listed = source.listAvailableArtifacts(EnumSet.allOf(SonarLanguage.class)); // Only one unique plugin key for C-family: "cpp" assertThat(listed).hasSize(5); assertThat(listed) .extracting(AvailableArtifact::key) .containsOnly("cpp", "csharp", "omnisharp-mono", "omnisharp-net472", "omnisharp-net6"); } private BinariesArtifactSource buildSource() { var userPaths = mock(UserPaths.class); when(userPaths.getStorageRoot()).thenReturn(tempDir); return new BinariesArtifactSource(userPaths, httpClientProvider, eventPublisher, Executors.newCachedThreadPool(), signatureVerifier, cacheManager); } private void mockSuccessfulHttpClient() throws Exception { var jarBytes = createMinimalPluginJarBytes("cpp", "1.0.0"); var httpClient = mock(HttpClient.class); var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(200); when(response.isSuccessful()).thenReturn(true); when(response.bodyAsStream()).thenReturn(new ByteArrayInputStream(jarBytes)); when(httpClient.get(anyString())).thenReturn(response); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); } private void mockBlockingHttpClient(CountDownLatch proceedLatch) { var httpClient = mock(HttpClient.class); var response = mock(HttpClient.Response.class); when(response.isSuccessful()).thenReturn(true); when(response.code()).thenReturn(200); when(response.bodyAsStream()).thenAnswer(inv -> awaitAndReturnEmpty(proceedLatch)); when(httpClient.get(anyString())).thenReturn(response); when(httpClientProvider.getHttpClientWithoutAuth()).thenReturn(httpClient); } private static InputStream awaitAndReturnEmpty(CountDownLatch latch) throws InterruptedException { latch.await(); return InputStream.nullInputStream(); } private static byte[] createMinimalPluginJarBytes(String pluginKey, String pluginVersion) throws IOException { var tempJar = Files.createTempFile("test-plugin", ".jar"); try { var manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue("Plugin-Key", pluginKey); manifest.getMainAttributes().putValue("Plugin-Version", pluginVersion); try (var jos = new JarOutputStream(Files.newOutputStream(tempJar), manifest)) { // minimal JAR with only the manifest } return Files.readAllBytes(tempJar); } finally { Files.deleteIfExists(tempJar); } } private static ResolvedArtifact downloading() { return new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null); } private static PluginStatus activeStatus(SonarLanguage lang, Path path) { return PluginStatus.forLanguage(lang, ArtifactState.ACTIVE, ArtifactOrigin.ON_DEMAND, Version.create(BinariesArtifact.CFAMILY_PLUGIN.version()), null, path, null); } private static PluginStatus failedStatus(SonarLanguage lang) { return PluginStatus.forLanguage(lang, ArtifactState.FAILED, null, null, null, null, null); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesLocalCacheManagerTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class BinariesLocalCacheManagerTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private final BinariesLocalCacheManager underTest = new BinariesLocalCacheManager(); @Test void should_do_nothing_when_cache_directory_does_not_exist() { var missing = tempDir.resolve("does-not-exist"); Assertions.assertDoesNotThrow(() -> underTest.cleanupOldVersions(missing, "1.0")); } @Test void should_not_delete_current_version_directory() throws IOException { var cacheDir = tempDir.resolve("cpp"); var currentVersionDir = cacheDir.resolve("1.0"); Files.createDirectories(currentVersionDir); setOldModificationTime(currentVersionDir); underTest.cleanupOldVersions(cacheDir, "1.0"); assertThat(currentVersionDir).exists(); } @Test void should_delete_old_version_directory() throws IOException { var cacheDir = tempDir.resolve("cpp"); var oldVersionDir = cacheDir.resolve("0.9"); Files.createDirectories(oldVersionDir); setOldModificationTime(oldVersionDir); underTest.cleanupOldVersions(cacheDir, "1.0"); assertThat(oldVersionDir).doesNotExist(); } @Test void should_not_delete_recently_modified_version_directory() throws IOException { var cacheDir = tempDir.resolve("cpp"); var recentVersionDir = cacheDir.resolve("0.9"); Files.createDirectories(recentVersionDir); // Modification time is "now" by default — well within retention period underTest.cleanupOldVersions(cacheDir, "1.0"); assertThat(recentVersionDir).exists(); } @Test void should_delete_only_old_version_directories_in_mixed_scenario() throws IOException { var cacheDir = tempDir.resolve("cpp"); var currentDir = cacheDir.resolve("1.0"); var oldDir = cacheDir.resolve("0.8"); var recentDir = cacheDir.resolve("0.9"); Files.createDirectories(currentDir); Files.createDirectories(oldDir); Files.createDirectories(recentDir); setOldModificationTime(currentDir); // current is old but should be kept by name setOldModificationTime(oldDir); // recentDir stays with current modification time underTest.cleanupOldVersions(cacheDir, "1.0"); assertThat(currentDir).exists(); assertThat(oldDir).doesNotExist(); assertThat(recentDir).exists(); } @Test void should_do_nothing_when_cache_directory_is_empty() throws IOException { var cacheDir = tempDir.resolve("cpp"); Files.createDirectories(cacheDir); underTest.cleanupOldVersions(cacheDir, "1.0"); assertThat(cacheDir).exists(); } private static void setOldModificationTime(Path path) throws IOException { var oldTime = Instant.now().minus(61, ChronoUnit.DAYS); Files.setLastModifiedTime(path, FileTime.from(oldTime)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/binaries/BinariesSignatureVerifierTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.binaries; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class BinariesSignatureVerifierTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private final BinariesSignatureVerifier underTest = new BinariesSignatureVerifier(); @Test void should_return_false_when_jar_signature_not_found() throws IOException { var jarPath = createMinimalPluginJar("nonexistent", "1.0.0"); assertThat(underTest.verify(jarPath, "nonexistent")).isFalse(); } @Test void should_return_false_when_jar_is_tampered() throws IOException { var tamperedJar = tempDir.resolve("sonar-cpp-plugin-tampered.jar"); Files.write(tamperedJar, "tampered content".getBytes()); Assertions.assertThat(underTest.verify(tamperedJar, BinariesArtifact.CFAMILY_PLUGIN)).isFalse(); } @ParameterizedTest @ValueSource(strings = {"cpp-unknownkey", "cpp-corrupt", "cpp-nosig"}) void should_return_false_for_invalid_signatures(String pluginKey) throws IOException { var jarPath = createMinimalPluginJar(pluginKey, "1.0.0"); // Verify it fails with a nonexistent signature path assertThat(underTest.verify(jarPath, "ondemand/sonar-cpp-plugin-nonexistent.jar.asc")).isFalse(); } private Path createMinimalPluginJar(String pluginKey, String pluginVersion) throws IOException { var target = tempDir.resolve("sonar-" + pluginKey + "-plugin-test.jar"); var manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue("Plugin-Key", pluginKey); manifest.getMainAttributes().putValue("Plugin-Version", pluginVersion); try (var jos = new JarOutputStream(Files.newOutputStream(target), manifest)) { // minimal JAR with only the manifest } return target; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/embedded/EmbeddedPluginSourceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.embedded; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.source.AvailableArtifact; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class EmbeddedPluginSourceTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; // --- forConnected() --- @Test void list_AvailablePlugins_should_return_active_embedded_in_connected_mode_when_plugin_key_is_in_map() throws IOException { var javaJar = createJar("sonar-java-plugin.jar"); var source = EmbeddedPluginSource.forConnected(mockParams(Set.of(), Map.of(SonarPlugin.JAVA.getKey(), javaJar))); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo(SonarPlugin.JAVA.getKey()); } @Test void list_AvailablePlugins_should_return_empty_in_connected_mode_when_plugin_key_is_absent() { var source = EmbeddedPluginSource.forConnected(mockParams(Set.of(), Map.of())); assertThat(source.listAvailableArtifacts(Set.of())).isEmpty(); } @Test void list_AvailablePlugins_should_include_companion_in_connected_mode_when_present_in_connected_mode_embedded_paths() throws IOException { var omnisharpJar = createJar("sonarlint-omnisharp-plugin.jar", "omnisharp"); var source = EmbeddedPluginSource.forConnected(mockParams(Set.of(), Map.of("omnisharp", omnisharpJar))); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("omnisharp"); } // --- forStandalone() --- @Test void list_AvailablePlugins_should_return_active_embedded_in_standalone_when_jar_contains_plugin_key_manifest() throws IOException { var javaJar = createJar("sonar-java-plugin.jar", "java"); var source = EmbeddedPluginSource.forStandalone(mockParams(Set.of(javaJar), Map.of())); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("java"); } @Test void list_AvailablePlugins_should_return_empty_in_standalone_when_embedded_paths_are_empty() { var source = EmbeddedPluginSource.forStandalone(mockParams(Set.of(), Map.of())); assertThat(source.listAvailableArtifacts(Set.of())).isEmpty(); } @Test void list_AvailablePlugins_should_throw_when_duplicate_plugin_keys_are_found() throws IOException { var javaJar1 = createJar("sonar-java-plugin.jar", "java"); var javaJar2 = createJar("sonar-java-plugin-2.jar", "java"); var params = mockParams(Set.of(javaJar1, javaJar2), Map.of()); assertThatThrownBy(() -> EmbeddedPluginSource.forStandalone(params)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Multiple embedded plugins found with the same key for paths"); } // --- Companion plugins are included naturally in list() --- @Test void list_AvailablePlugins_should_include_companion_plugins_in_standalone_along_with_language_plugins() throws IOException { var omnisharpJar = createJar("sonarlint-omnisharp-plugin.jar", "omnisharp"); var javaJar = createJar("sonar-java-plugin.jar", "java"); var source = EmbeddedPluginSource.forStandalone(mockParams(Set.of(omnisharpJar, javaJar), Map.of())); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(2); assertThat(result.stream().map(AvailableArtifact::key)) .containsExactlyInAnyOrder("omnisharp", "java"); } @Test void list_AvailablePlugins_should_not_include_html_plugin_key_when_manifest_says_web() throws IOException { var htmlJar = createJar("sonar-html-plugin.jar", "web"); var source = EmbeddedPluginSource.forStandalone(mockParams(Set.of(htmlJar), Map.of())); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("web"); } private static InitializeParams mockParams(Set embeddedPaths, Map connectedPaths) { var params = mock(InitializeParams.class); when(params.getEmbeddedPluginPaths()).thenReturn(embeddedPaths); when(params.getConnectedModeEmbeddedPluginPathsByKey()).thenReturn(connectedPaths); return params; } private Path createJar(String name) throws IOException { return createJar(name, name.replace("sonar-", "").replaceAll("-plugin\\.jar|-oss\\.jar|-standalone\\.jar", "")); } private Path createJar(String name, String pluginKey) throws IOException { var path = tempDir.resolve(name); var manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue("Plugin-Key", pluginKey); try (var jos = new JarOutputStream(Files.newOutputStream(path), manifest)) { // empty JAR with manifest } return path; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginDownloaderTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.ConnectionKind; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.event.PluginStatusUpdateEvent; import org.sonarsource.sonarlint.core.plugin.PluginStatus; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.repository.connection.AbstractConnectionConfiguration; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ServerPluginDownloaderTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private StorageService storageService; private ConnectionConfigurationRepository connectionRepo; private SonarQubeClientManager sonarQubeClientManager; private ApplicationEventPublisher eventPublisher; private ExecutorService downloadExecutor; private PluginsStorage pluginsStorage; private Path javaJar; @BeforeEach void setUp() { storageService = mock(StorageService.class); connectionRepo = mock(ConnectionConfigurationRepository.class); sonarQubeClientManager = mock(SonarQubeClientManager.class); eventPublisher = mock(ApplicationEventPublisher.class); var connectionStorage = mock(ConnectionStorage.class); pluginsStorage = mock(PluginsStorage.class); javaJar = Path.of("sonar-java-plugin.jar"); when(storageService.connection("conn")).thenReturn(connectionStorage); when(connectionStorage.plugins()).thenReturn(pluginsStorage); var connection = mock(AbstractConnectionConfiguration.class); when(connection.getKind()).thenReturn(ConnectionKind.SONARQUBE); when(connectionRepo.getConnectionById("conn")).thenReturn(connection); } @Test void should_publish_synced_event_after_language_plugin_download_succeeds() { downloadExecutor = Executors.newSingleThreadExecutor(); try { var downloader = new ServerPluginDownloader(storageService, sonarQubeClientManager, connectionRepo, eventPublisher, downloadExecutor); when(pluginsStorage.getStoredPluginPathsByKey()).thenReturn(Map.of(SonarPlugin.JAVA.getKey(), javaJar)); var serverPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey()); downloader.schedulePluginDownload("conn", serverPlugin); var expectedEvent = new PluginStatusUpdateEvent("conn", List.of(PluginStatus.forLanguage(SonarLanguage.JAVA, ArtifactState.SYNCED, ArtifactOrigin.SONARQUBE_SERVER, null, null, javaJar, null))); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> verify(eventPublisher).publishEvent(expectedEvent)); } finally { downloadExecutor.shutdownNow(); } } @Test void should_publish_failed_event_when_language_plugin_download_fails() { downloadExecutor = Executors.newSingleThreadExecutor(); try { var downloader = new ServerPluginDownloader(storageService, sonarQubeClientManager, connectionRepo, eventPublisher, downloadExecutor); var serverPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey()); doThrow(new RuntimeException("Download failed")).when(sonarQubeClientManager).withActiveClient(any(), any()); downloader.schedulePluginDownload("conn", serverPlugin); var expectedEvent = new PluginStatusUpdateEvent("conn", List.of(PluginStatus.failed(SonarLanguage.JAVA))); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> verify(eventPublisher).publishEvent(expectedEvent)); } finally { downloadExecutor.shutdownNow(); } } @Test void should_deduplicate_concurrent_plugin_downloads() { var mockedExecutor = mock(ExecutorService.class); var downloader = new ServerPluginDownloader(storageService, sonarQubeClientManager, connectionRepo, eventPublisher, mockedExecutor); var serverPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey()); downloader.schedulePluginDownload("conn", serverPlugin); downloader.schedulePluginDownload("conn", serverPlugin); verify(mockedExecutor, times(1)).execute(any(Runnable.class)); } @Test void should_perform_synchronous_download_and_return_state() { downloadExecutor = mock(ExecutorService.class); var downloader = new ServerPluginDownloader(storageService, sonarQubeClientManager, connectionRepo, eventPublisher, downloadExecutor); var serverPlugin = mockServerPlugin("custom-plugin"); var state = downloader.downloadPluginSync("conn", serverPlugin); assertThat(state).isEqualTo(ArtifactState.SYNCED); verify(sonarQubeClientManager).withActiveClient(any(), any()); } private static ServerPlugin mockServerPlugin(String pluginKey) { var plugin = mock(ServerPlugin.class); when(plugin.getKey()).thenReturn(pluginKey); return plugin; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginSourceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactOrigin; import org.sonarsource.sonarlint.core.plugin.source.ArtifactState; import org.sonarsource.sonarlint.core.plugin.source.LoadResult; import org.sonarsource.sonarlint.core.plugin.source.ResolvedArtifact; import org.sonarsource.sonarlint.core.serverapi.exception.ServerRequestException; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ServerPluginSourceTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private Path javaJar; private Path iacJar; private StorageService storageService; private ConnectionStorage connectionStorage; private PluginsStorage pluginsStorage; private ServerPluginsCache serverPluginsCache; private ServerPluginDownloader downloader; @BeforeEach void setUp() throws IOException { javaJar = Files.createFile(tempDir.resolve("sonar-java-plugin.jar")); iacJar = Files.createFile(tempDir.resolve("sonar-iac-plugin.jar")); storageService = mock(StorageService.class); connectionStorage = mock(ConnectionStorage.class); pluginsStorage = mock(PluginsStorage.class); serverPluginsCache = mock(ServerPluginsCache.class); downloader = mock(ServerPluginDownloader.class); when(downloader.schedulePluginDownload(any(), any())).thenReturn(null); when(connectionStorage.plugins()).thenReturn(pluginsStorage); when(pluginsStorage.getStoredPluginsByKey()).thenReturn(Map.of()); } // --- load() — connected mode --- @Test void load_should_return_synced_from_storage_when_plugin_is_in_storage_but_not_on_server() { mockStorage("conn"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), javaJar, "hash"); mockServerPlugins("conn", List.of()); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var expected = resolved(ArtifactState.SYNCED, javaJar, ArtifactOrigin.SONARQUBE_SERVER); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), expected); } @Test void load_should_return_empty_when_plugin_is_not_in_storage_and_not_on_server() { mockStorage("conn"); when(pluginsStorage.getStoredPluginPathsByKey()).thenReturn(Map.of()); mockServerPlugins("conn", List.of()); var source = createSource("conn"); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPlugin.JAVA.getKey()); } @Test void load_should_trigger_download_when_stored_jar_does_not_exist_on_disk() { mockStorage("conn"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), tempDir.resolve("missing.jar"), "hash"); var serverPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey(), "hash"); mockServerPlugins("conn", List.of(serverPlugin)); var source = createSource("conn"); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null)); verify(downloader).schedulePluginDownload("conn", serverPlugin); } @Test void load_should_return_downloading_when_plugin_is_not_in_storage_but_on_server() { mockStorage("conn"); when(pluginsStorage.getStoredPluginPathsByKey()).thenReturn(Map.of()); var serverPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey()); mockServerPlugins("conn", List.of(serverPlugin)); var source = createSource("conn"); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), new ResolvedArtifact(ArtifactState.DOWNLOADING, null, null, null, null)); verify(downloader).schedulePluginDownload("conn", serverPlugin); } @Test void load_should_call_cleanUpUnknownPlugins_with_empty_list_when_no_server_plugins_win() { mockStorage("conn"); mockServerPlugins("conn", List.of()); var source = createSource("conn"); source.load(Set.of()); verify(pluginsStorage).cleanUpUnknownPlugins(List.of()); } @Test void load_should_call_cleanUpUnknownPlugins_with_server_plugins_that_won() { mockStorage("conn"); var javaPlugin = mockServerPlugin(SonarPlugin.JAVA.getKey(), "hash"); mockServerPlugins("conn", List.of(javaPlugin)); var source = createSource("conn"); source.load(Set.of(SonarPlugin.JAVA.getKey())); verify(pluginsStorage).cleanUpUnknownPlugins(List.of(javaPlugin)); } @Test void load_should_not_call_cleanUpUnknownPlugins_when_server_is_not_accessible() { mockStorage("conn"); when(serverPluginsCache.getPlugins("conn")).thenThrow(new ServerRequestException("Connection refused")); var source = createSource("conn"); source.load(Set.of(SonarPlugin.JAVA.getKey())); verify(pluginsStorage, never()).cleanUpUnknownPlugins(any()); } @Test void load_should_return_empty_when_server_plugin_list_AvailablePlugins_request_fails_and_no_storage() { mockStorage("conn"); when(serverPluginsCache.getPlugins("conn")).thenThrow(new ServerRequestException("Connection refused")); var source = createSource("conn"); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).doesNotContainKey(SonarPlugin.JAVA.getKey()); } @Test void load_should_return_synced_from_storage_when_server_plugin_list_AvailablePlugins_request_fails() { mockStorage("conn"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), javaJar, "hash"); when(serverPluginsCache.getPlugins("conn")).thenThrow(new ServerRequestException("Connection refused")); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), new ResolvedArtifact(ArtifactState.SYNCED, javaJar, ArtifactOrigin.SONARQUBE_SERVER, null, null)); } @Test void load_should_return_synced_with_sonarqube_server_source() { mockStorage("conn"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), javaJar, "hash"); mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.JAVA.getKey(), "hash"))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var expected = resolved(ArtifactState.SYNCED, javaJar, ArtifactOrigin.SONARQUBE_SERVER); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), expected); } @Test void load_should_return_synced_with_sonarqube_cloud_source() { mockStorage("cloud"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), javaJar, "hash"); mockServerPlugins("cloud", List.of(mockServerPlugin(SonarPlugin.JAVA.getKey(), "hash"))); when(downloader.sourceFor("cloud")).thenReturn(ArtifactOrigin.SONARQUBE_CLOUD); var source = createSource("cloud"); var expected = resolved(ArtifactState.SYNCED, javaJar, ArtifactOrigin.SONARQUBE_CLOUD); var result = source.load(Set.of(SonarPlugin.JAVA.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.JAVA.getKey(), expected); } // --- ANSIBLE/GITHUBACTIONS use "iac" plugin key --- @Test void load_should_resolve_iac_plugin_for_iac_language() { mockStorage("conn"); mockStoredPlugin("iac", iacJar, "hash"); mockServerPlugins("conn", List.of(mockServerPlugin("iac", "hash"))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var expected = resolved(ArtifactState.SYNCED, iacJar, ArtifactOrigin.SONARQUBE_SERVER); var result = source.load(Set.of(SonarPlugin.IAC.getKey())); assertThat(result.resolvedArtifactsByKey()).containsEntry(SonarPlugin.IAC.getKey(), expected); } // --- listAvailableArtifacts() --- @Test void listAvailableArtifacts_should_include_language_plugin_when_language_is_enabled() { mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.JAVA.getKey()))); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.JAVA)); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo(SonarPlugin.JAVA.getKey()); } @Test void listAvailableArtifacts_should_exclude_language_plugin_when_language_not_enabled() { mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.JAVA.getKey()))); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).isEmpty(); } @Test void listAvailableArtifacts_should_include_csharpenterprise_when_csharp_is_enabled() { var csEnterprise = mockServerPlugin("csharpenterprise"); when(csEnterprise.isSonarLintSupported()).thenReturn(false); mockServerPlugins("conn", List.of(csEnterprise)); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.CS)); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("csharpenterprise"); } @Test void listAvailableArtifacts_should_exclude_csharpenterprise_when_csharp_not_enabled() { var csEnterprise = mockServerPlugin("csharpenterprise"); when(csEnterprise.isSonarLintSupported()).thenReturn(false); mockServerPlugins("conn", List.of(csEnterprise)); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).isEmpty(); } @Test void listAvailableArtifacts_should_mark_csharpenterprise_as_enterprise() { var csEnterprise = mockServerPlugin("csharpenterprise"); mockServerPlugins("conn", List.of(csEnterprise)); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.CS)); assertThat(result).hasSize(1); assertThat(result.get(0).isEnterprise()).isTrue(); } @Test void listAvailableArtifacts_should_include_companion_plugin_when_sonar_lint_supported() { var companion = mockServerPlugin("my-companion-plugin"); when(companion.isSonarLintSupported()).thenReturn(true); mockServerPlugins("conn", List.of(companion)); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo("my-companion-plugin"); } @Test void listAvailableArtifacts_should_exclude_companion_plugin_when_not_sonar_lint_supported() { var companion = mockServerPlugin("some-unknown-plugin"); when(companion.isSonarLintSupported()).thenReturn(false); mockServerPlugins("conn", List.of(companion)); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of()); assertThat(result).isEmpty(); } @Test void listAvailableArtifacts_should_return_empty_when_server_request_fails_and_nothing_stored() { mockStorage("conn"); when(serverPluginsCache.getPlugins("conn")).thenThrow(new RuntimeException("Connection refused")); var source = createSource("conn"); assertThat(source.listAvailableArtifacts(Set.of(SonarLanguage.JAVA))).isEmpty(); } @Test void listAvailableArtifacts_should_fall_back_to_stored_plugins_when_server_request_fails() { mockStorage("conn"); mockStoredPlugin(SonarPlugin.JAVA.getKey(), javaJar, "hash"); when(serverPluginsCache.getPlugins("conn")).thenThrow(new RuntimeException("Connection refused")); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.JAVA)); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo(SonarPlugin.JAVA.getKey()); } // --- Enterprise detection for same-key plugins (GO, IAC, TEXT) --- @Test void listAvailableArtifacts_should_mark_go_as_enterprise_when_server_version_qualifies() { mockStorage("conn"); mockServerVersion("2025.2"); mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.GO.getKey()))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.GO)); assertThat(result).hasSize(1); assertThat(result.get(0).key()).isEqualTo(SonarPlugin.GO.getKey()); assertThat(result.get(0).isEnterprise()).isTrue(); } @Test void listAvailableArtifacts_should_not_mark_go_as_enterprise_when_server_version_too_old() { mockStorage("conn"); mockServerVersion("2025.1"); mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.GO.getKey()))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_SERVER); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.GO)); assertThat(result).hasSize(1); assertThat(result.get(0).isEnterprise()).isFalse(); } @Test void listAvailableArtifacts_should_mark_go_as_enterprise_on_sonarqube_cloud() { mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.GO.getKey()))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_CLOUD); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.GO)); assertThat(result).hasSize(1); assertThat(result.get(0).isEnterprise()).isTrue(); } @Test void listAvailableArtifacts_should_not_mark_java_as_enterprise() { mockServerPlugins("conn", List.of(mockServerPlugin(SonarPlugin.JAVA.getKey()))); when(downloader.sourceFor("conn")).thenReturn(ArtifactOrigin.SONARQUBE_CLOUD); var source = createSource("conn"); var result = source.listAvailableArtifacts(Set.of(SonarLanguage.JAVA)); assertThat(result).hasSize(1); assertThat(result.get(0).isEnterprise()).isFalse(); } private ServerPluginSource createSource(String connectionId) { return new ServerPluginSource(connectionId, storageService, serverPluginsCache, downloader); } private void mockStorage(String connectionId) { when(storageService.connection(connectionId)).thenReturn(connectionStorage); } private void mockServerVersion(String version) { var serverInfoStorage = mock(ServerInfoStorage.class); var storedServerInfo = mock(StoredServerInfo.class); when(connectionStorage.serverInfo()).thenReturn(serverInfoStorage); when(serverInfoStorage.read()).thenReturn(Optional.of(storedServerInfo)); when(storedServerInfo.version()).thenReturn(Version.create(version)); } private void mockServerPlugins(String connectionId, List plugins) { when(serverPluginsCache.getPlugins(connectionId)).thenReturn(Optional.of(plugins)); } private static ServerPlugin mockServerPlugin(String pluginKey) { var plugin = mock(ServerPlugin.class); when(plugin.getKey()).thenReturn(pluginKey); return plugin; } private static ServerPlugin mockServerPlugin(String pluginKey, String hash) { var plugin = mock(ServerPlugin.class); when(plugin.getKey()).thenReturn(pluginKey); when(plugin.getHash()).thenReturn(hash); return plugin; } private void mockStoredPlugin(String pluginKey, Path jarPath, String hash) { when(pluginsStorage.getStoredPluginsByKey()).thenReturn(Map.of(pluginKey, new StoredPlugin(pluginKey, hash, jarPath))); } private static ResolvedArtifact resolved(ArtifactState state, Path path, ArtifactOrigin source) { return new ResolvedArtifact(state, path, source, null, null); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/plugin/source/server/ServerPluginsCacheTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.source.server; import java.util.List; import java.util.Optional; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationRemovedEvent; import org.sonarsource.sonarlint.core.event.ConnectionConfigurationUpdatedEvent; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ServerPluginsCacheTest { private SonarQubeClientManager sonarQubeClientManager; private ServerPluginsCache cache; @BeforeEach void setUp() { sonarQubeClientManager = mock(SonarQubeClientManager.class); cache = new ServerPluginsCache(sonarQubeClientManager); } @Test void should_return_cached_plugins_on_second_call() { var plugins = List.of(mockPlugin("java")); mockApiResponse("conn", plugins); cache.getPlugins("conn"); cache.getPlugins("conn"); verifyApiCalledOnce("conn"); } @Test void should_invalidate_on_connection_removed() { var plugins = List.of(mockPlugin("java")); mockApiResponse("conn", plugins); cache.getPlugins("conn"); cache.connectionRemoved(new ConnectionConfigurationRemovedEvent("conn")); cache.getPlugins("conn"); verifyApiCalledTimes("conn", 2); } @Test void should_invalidate_on_connection_updated() { var plugins = List.of(mockPlugin("java")); mockApiResponse("conn", plugins); cache.getPlugins("conn"); cache.connectionUpdated(new ConnectionConfigurationUpdatedEvent("conn")); cache.getPlugins("conn"); verifyApiCalledTimes("conn", 2); } @Test void should_cache_per_connection_id() { mockApiResponse("conn1", List.of(mockPlugin("java"))); mockApiResponse("conn2", List.of(mockPlugin("python"))); var result1 = cache.getPlugins("conn1"); var result2 = cache.getPlugins("conn2"); assertThat(result1).isPresent(); assertThat(result2).isPresent(); assertThat(result1.get()).extracting(ServerPlugin::getKey).containsExactly("java"); assertThat(result2.get()).extracting(ServerPlugin::getKey).containsExactly("python"); } @Test void should_return_empty_when_connection_not_found() { when(sonarQubeClientManager.withActiveClientAndReturn(eq("unknown"), any(Function.class))) .thenReturn(Optional.empty()); var result = cache.getPlugins("unknown"); assertThat(result).isEmpty(); } @SuppressWarnings("unchecked") private void mockApiResponse(String connectionId, List plugins) { when(sonarQubeClientManager.withActiveClientAndReturn(eq(connectionId), any(Function.class))) .thenAnswer(invocation -> { Function fn = invocation.getArgument(1); var api = mock(org.sonarsource.sonarlint.core.serverapi.ServerApi.class); var pluginsApi = mock(org.sonarsource.sonarlint.core.serverapi.plugins.PluginsApi.class); when(api.plugins()).thenReturn(pluginsApi); when(pluginsApi.getInstalled(any())).thenReturn(plugins); return Optional.of(fn.apply(api)); }); } @SuppressWarnings("unchecked") private void verifyApiCalledOnce(String connectionId) { verify(sonarQubeClientManager, times(1)).withActiveClientAndReturn(eq(connectionId), any(Function.class)); } @SuppressWarnings("unchecked") private void verifyApiCalledTimes(String connectionId, int times) { verify(sonarQubeClientManager, times(times)).withActiveClientAndReturn(eq(connectionId), any(Function.class)); } private static ServerPlugin mockPlugin(String key) { var plugin = mock(ServerPlugin.class); when(plugin.getKey()).thenReturn(key); return plugin; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/progress/ClientAwareTaskManagerTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.progress; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.api.progress.CanceledException; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ClientAwareTaskManagerTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void it_should_throw_when_interrupted() throws InterruptedException { var client = mock(SonarLintRpcClient.class); when(client.startProgress(any())).thenReturn(new CompletableFuture<>()); var taskManager = new ClientAwareTaskManager(client); var caughtException = new AtomicReference(); var thread = new Thread(() -> { try { taskManager.createAndRunTask("configScopeId", UUID.randomUUID(), "Title", null, true, true, progressIndicator -> { }, new SonarLintCancelMonitor()); } catch (Exception e) { caughtException.set(e); } }); thread.start(); Thread.sleep(500); thread.interrupt(); await().untilAsserted(() -> assertThat(caughtException.get()).isInstanceOf(CanceledException.class)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixServiceTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.remediation.aicodefix; import java.nio.file.Path; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.SonarQubeClientManager; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository; import org.sonarsource.sonarlint.core.repository.reporting.PreviouslyRaisedFindingsRepository; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFix; import org.sonarsource.sonarlint.core.serverconnection.aicodefix.AiCodeFixRepository; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; import org.springframework.context.ApplicationEventPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; /** * This test verifies that AiCodeFixService.getFeature() reads settings * from the H2-backed AiCodeFixRepository (and does not rely on file-based StorageService). */ class AiCodeFixServiceTest { @RegisterExtension static SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private SonarLintDatabase db; @AfterEach void tearDown() { if (db != null) { db.shutdown(); } } @Test void getFeature_reads_from_h2_repository() { db = new SonarLintDatabase(tempDir); var aiCodeFixRepo = new AiCodeFixRepository(db.dsl()); var connectionId = "conn-1"; var projectKey = "project-A"; aiCodeFixRepo.upsert(new AiCodeFix( connectionId, Set.of("xml:S3421"), true, AiCodeFix.Enablement.ENABLED_FOR_ALL_PROJECTS, Set.of(projectKey))); var connectionRepository = mock(ConnectionConfigurationRepository.class); var configurationRepository = mock(ConfigurationRepository.class); var sonarQubeClientManager = mock(SonarQubeClientManager.class); var previouslyRaisedFindingsRepository = mock(PreviouslyRaisedFindingsRepository.class); var clientFileSystemService = mock(ClientFileSystemService.class); var eventPublisher = mock(ApplicationEventPublisher.class); var taintService = mock(TaintVulnerabilityTrackingService.class); var service = new AiCodeFixService(connectionRepository, configurationRepository, sonarQubeClientManager, previouslyRaisedFindingsRepository, clientFileSystemService, eventPublisher, taintService, aiCodeFixRepo); var binding = new Binding(connectionId, projectKey); Optional featureOpt = service.getFeature(binding); assertThat(featureOpt).isPresent(); var feature = featureOpt.get(); assertThat(feature.settings().supportedRules()).contains("xml:S3421"); assertThat(feature.settings().isFeatureEnabled(projectKey)).isTrue(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/BindingConfigurationTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class BindingConfigurationTest { @Test void test_isBound() { BindingConfiguration noBinding = BindingConfiguration.noBinding(); assertThat(noBinding.isBound()).isFalse(); BindingConfiguration noProjectKey = new BindingConfiguration("connection", null, true); assertThat(noProjectKey.isBound()).isFalse(); BindingConfiguration noConnection = new BindingConfiguration(null, "projectKey", true); assertThat(noConnection.isBound()).isFalse(); BindingConfiguration valid = new BindingConfiguration("connection", "projectKey", true); assertThat(valid.isBound()).isTrue(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/config/ConfigurationRepositoryTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.config; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ConfigurationRepositoryTest { private ConfigurationRepository configurationRepository; @BeforeEach void prepare() { configurationRepository = new ConfigurationRepository(); } @Test void it_should_not_find_any_binding_on_an_unknown_scope() { var binding = configurationRepository.getEffectiveBinding("id"); assertThat(binding).isEmpty(); } @Test void it_should_not_find_any_binding_on_an_unbound_scope() { configurationRepository.addOrReplace(new ConfigurationScope("id", null, true, "name"), BindingConfiguration.noBinding(true)); var binding = configurationRepository.getEffectiveBinding("id"); assertThat(binding).isEmpty(); } @Test void it_should_consider_the_binding_configured_on_a_scope_as_effective() { configurationRepository.addOrReplace(new ConfigurationScope("id", null, true, "name"), new BindingConfiguration("connectionId", "projectKey", true)); var binding = configurationRepository.getEffectiveBinding("id"); assertThat(binding) .hasValueSatisfying(b -> { assertThat(b.connectionId()).isEqualTo("connectionId"); assertThat(b.sonarProjectKey()).isEqualTo("projectKey"); }); } @Test void it_should_get_the_effective_binding_from_parent_if_child_is_unbound() { configurationRepository.addOrReplace(new ConfigurationScope("parentId", null, true, "name"), new BindingConfiguration("connectionId", "projectKey", true)); configurationRepository.addOrReplace(new ConfigurationScope("id", "parentId", true, "name"), new BindingConfiguration(null, null, true)); var binding = configurationRepository.getEffectiveBinding("id"); assertThat(binding) .hasValueSatisfying(b -> { assertThat(b.connectionId()).isEqualTo("connectionId"); assertThat(b.sonarProjectKey()).isEqualTo("projectKey"); }); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarCloudConnectionConfigurationTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.SonarCloudRegion; import static org.assertj.core.api.Assertions.assertThat; class SonarCloudConnectionConfigurationTest { @Test void testEqualsAndHashCode() { var underTest = new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true); assertThat(underTest) .isEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)) .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id2", "org1", SonarCloudRegion.EU, true)) .isNotEqualTo(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org2", SonarCloudRegion.EU, true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server1", true)) .hasSameHashCodeAs(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), "id1", "org1", SonarCloudRegion.EU, true)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/connection/SonarQubeConnectionConfigurationTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.connection; import java.net.URI; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.SonarCloudRegion; import static org.assertj.core.api.Assertions.assertThat; class SonarQubeConnectionConfigurationTest { @Test void test_isSameServerUrl() { var underTest = new SonarQubeConnectionConfiguration("id", "https://mycompany.org", true); assertThat(underTest.isSameServerUrl("https://mycompany.org")).isTrue(); // URL are case insensitive assertThat(underTest.isSameServerUrl("https://Mycompany.Org")).isTrue(); // We can ignore trailing slash difference, as we are looking for a base URL assertThat(underTest.isSameServerUrl("https://mycompany.org/")).isTrue(); // Protocol difference, let's play it safe and not assume it is the same server assertThat(underTest.isSameServerUrl("http://mycompany.org")).isFalse(); // Different path assertThat(underTest.isSameServerUrl("https://mycompany.org/sonarqube")).isFalse(); // Different domain assertThat(underTest.isSameServerUrl("https://sq.mycompany.org")).isFalse(); } @Test void testEqualsAndHashCode() { var underTest = new SonarQubeConnectionConfiguration("id1", "http://server1", true); assertThat(underTest) .isEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server1", true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id2", "http://server1", true)) .isNotEqualTo(new SonarQubeConnectionConfiguration("id1", "http://server2", true)) .isNotEqualTo(new SonarCloudConnectionConfiguration(URI.create("http://server1"), URI.create("http://server1"), "id1", "org1", SonarCloudRegion.EU, true)) .hasSameHashCodeAs(new SonarQubeConnectionConfiguration("id1", "http://server1", true)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/repository/rules/RulesRepositoryTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.repository.rules; import java.util.Optional; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rules.RulesExtractionHelper; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage; import org.sonarsource.sonarlint.core.storage.StorageService; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class RulesRepositoryTest { @Test void it_should_not_touch_storage_after_rules_are_lazily_loaded_in_connected_mode() { var storageService = mock(StorageService.class); var rulesRepository = new RulesRepository(mock(RulesExtractionHelper.class), storageService); var connectionStorage = mock(ConnectionStorage.class); when(storageService.connection("connection")).thenReturn(connectionStorage); var serverInfoStorage = mock(ServerInfoStorage.class); when(connectionStorage.serverInfo()).thenReturn(serverInfoStorage); when(serverInfoStorage.read()).thenReturn(Optional.empty()); rulesRepository.getRule("connection", "rule"); reset(storageService); rulesRepository.getRule("connection", "rule"); verifyNoInteractions(storageService); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RuleDetailsAdapterTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.CleanCodeAttributeCategory; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter.adapt; class RuleDetailsAdapterTests { @Test void it_should_adapt_all_cca_enum_values() { for (var cca : CleanCodeAttribute.values()) { var adapted = adapt(cca); assertThat(adapted.name()).isEqualTo(cca.name()); } } @Test void it_should_adapt_all_ccac_enum_values() { for (var ccac : CleanCodeAttributeCategory.values()) { var adapted = adapt(ccac); assertThat(adapted.name()).isEqualTo(ccac.name()); } } @Test void it_should_adapt_all_severity_enum_values() { for (var s : IssueSeverity.values()) { var adapted = adapt(s); assertThat(adapted.name()).isEqualTo(s.name()); } } @Test void it_should_adapt_all_ruletype_enum_values() { for (var t : RuleType.values()) { var adapted = adapt(t); assertThat(adapted.name()).isEqualTo(t.name()); } } @Test void it_should_adapt_all_language_enum_values() { for (var l : SonarLanguage.values()) { var adapted = adapt(l); assertThat(adapted.name()).isEqualTo(l.name()); } } @Test void it_should_adapt_all_impact_severity_enum_values() { for (var is : ImpactSeverity.values()) { var adapted = adapt(is); assertThat(adapted.name()).isEqualTo(is.name()); } } @Test void it_should_adapt_all_software_quality_enum_values() { for (var sq : SoftwareQuality.values()) { var adapted = adapt(sq); assertThat(adapted.name()).isEqualTo(sq.name()); } } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesFixtures.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.server.rule.RuleParamType; import org.sonar.api.server.rule.RulesDefinition; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; public class RulesFixtures { public static SonarLintRuleDefinition aRule() { RulesDefinition.Context c = new RulesDefinition.Context(); var repository = c.createRepository("repo", SonarLanguage.JAVA.getSonarLanguageKey()); repository.createRule("ruleKey") .setName("ruleName") .setType(RuleType.BUG) .setCleanCodeAttribute(CleanCodeAttribute.TRUSTWORTHY) .setHtmlDescription("Hello, world!") .createParam("paramKey") .setName("paramName") .setType(RuleParamType.TEXT) .setDescription("paramDesc") .setDefaultValue("defaultValue"); repository.done(); var rule = c.repositories().get(0).rule("ruleKey"); return new SonarLintRuleDefinition(rule); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/rules/RulesServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rules; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.repository.rules.RulesRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDefinitionDto; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import org.sonarsource.sonarlint.core.storage.StorageService; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.rules.RulesFixtures.aRule; class RulesServiceTests { private RulesRepository rulesRepository; private RulesExtractionHelper extractionHelper; @BeforeEach void prepare() { extractionHelper = mock(RulesExtractionHelper.class); var storageService = mock(StorageService.class); rulesRepository = new RulesRepository(extractionHelper, storageService); } @Test void it_should_return_all_embedded_rules_from_the_repository() { when(extractionHelper.extractEmbeddedRules()).thenReturn(List.of(aRule())); var rulesService = new RulesService(rulesRepository); var embeddedRules = rulesService.listAllStandaloneRulesDefinitions().values(); assertThat(embeddedRules) .extracting(RuleDefinitionDto::getKey, RuleDefinitionDto::getName) .containsExactly(tuple("repo:ruleKey", "ruleName")); } @Test void it_should_only_override_overridden_impact_quality() { Map defaultImpacts = Map.of( SoftwareQuality.MAINTAINABILITY, ImpactSeverity.LOW, SoftwareQuality.RELIABILITY, ImpactSeverity.MEDIUM); List overriddenImpacts = List.of( new ImpactPayload("MAINTAINABILITY", "HIGH")); Map result = RuleDetails.mergeImpacts(defaultImpacts, overriddenImpacts); assertThat(result) .containsEntry(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH) .containsEntry(SoftwareQuality.RELIABILITY, ImpactSeverity.MEDIUM); } @Test void it_should_work_when_no_overridden_impacts() { Map defaultImpacts = Map.of( SoftwareQuality.MAINTAINABILITY, ImpactSeverity.LOW, SoftwareQuality.RELIABILITY, ImpactSeverity.MEDIUM); Map result = RuleDetails.mergeImpacts(defaultImpacts, List.of()); assertThat(result).isEqualTo(defaultImpacts); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/sca/DependencyRiskServiceTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sca; import java.util.UUID; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import static org.assertj.core.api.Assertions.assertThat; class DependencyRiskServiceTests { @Test void testBuildSonarQubeServerScaUrl() { var dependencyKey = UUID.randomUUID(); assertThat(DependencyRiskService.buildDependencyRiskBrowseUrl("myProject", "myBranch", dependencyKey, new EndpointParams("http://foo.com", "", false, null))) .isEqualTo(String.format("http://foo.com/dependency-risks/%s/what?id=myProject&branch=myBranch", dependencyKey)); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/smartnotifications/LastEventPollingTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.smartnotifications; import java.nio.file.Path; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.UserPaths; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil; import org.sonarsource.sonarlint.core.storage.SonarLintDatabaseService; import org.sonarsource.sonarlint.core.storage.StorageService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs; class LastEventPollingTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final ZonedDateTime STORED_DATE = ZonedDateTime.now().minusDays(5); private static final String PROJECT_KEY = "projectKey"; private static final String CONNECTION_ID = "connectionId"; private static final String FILE_NAME = "last_event_polling.pb"; @Test void should_retrieve_stored_last_event_polling(@TempDir Path tmpDir) { var storageFile = tmpDir.resolve(encodeForFs(CONNECTION_ID)).resolve("projects").resolve(encodeForFs(PROJECT_KEY)).resolve(FILE_NAME); FileUtils.mkdirs(storageFile.getParent()); ProtobufFileUtil.writeToFile(Sonarlint.LastEventPolling.newBuilder() .setLastEventPolling(STORED_DATE.toInstant().toEpochMilli()) .build(), storageFile); var databaseService = mock(SonarLintDatabaseService.class); var storage = new StorageService(userPathsFrom(tmpDir), databaseService); var lastEventPolling = new LastEventPolling(storage); var result = lastEventPolling.getLastEventPolling(CONNECTION_ID, PROJECT_KEY); assertThat(result).isEqualTo(STORED_DATE.truncatedTo(ChronoUnit.MILLIS)); } @Test void should_store_last_event_polling(@TempDir Path tmpDir) { var databaseService = mock(SonarLintDatabaseService.class); var storage = new StorageService(userPathsFrom(tmpDir), databaseService); var lastEventPolling = new LastEventPolling(storage); lastEventPolling.setLastEventPolling(STORED_DATE, CONNECTION_ID, PROJECT_KEY); var result = lastEventPolling.getLastEventPolling(CONNECTION_ID, PROJECT_KEY); assertThat(result).isEqualTo(STORED_DATE.truncatedTo(ChronoUnit.MILLIS)); } @Test void should_not_retrieve_stored_last_event_polling(@TempDir Path tmpDir) { var databaseService = mock(SonarLintDatabaseService.class); var storage = new StorageService(userPathsFrom(tmpDir), databaseService); var lastEventPolling = new LastEventPolling(storage); var result = lastEventPolling.getLastEventPolling(CONNECTION_ID, PROJECT_KEY); assertThat(result).isBeforeOrEqualTo(ZonedDateTime.now()).isAfter(ZonedDateTime.now().minusSeconds(3)); } private static UserPaths userPathsFrom(Path tmpDir) { var mock = mock(UserPaths.class); when(mock.getStorageRoot()).thenReturn(tmpDir); when(mock.getWorkDir()).thenReturn(tmpDir); return mock; } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/sync/BranchBindingTest.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.sync; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.Binding; import static org.assertj.core.api.Assertions.assertThat; class BranchBindingTest { @Test void testEquals() { var branchBinding = new BranchBinding(new Binding("connection", "projectKey"), "branch"); assertThat(branchBinding.equals(branchBinding)).isTrue(); assertThat(branchBinding.equals(null)).isFalse(); assertThat(branchBinding.equals(new Object())).isFalse(); assertThat(branchBinding.equals(new BranchBinding(new Binding("connection2", "projectKey"), "branch"))).isFalse(); assertThat(branchBinding.equals(new BranchBinding(new Binding("connection", "projectKey2"), "branch"))).isFalse(); assertThat(branchBinding.equals(new BranchBinding(new Binding("connection", "projectKey"), "branch2"))).isFalse(); assertThat(branchBinding.equals(new BranchBinding(new Binding("connection", "projectKey"), "branch"))).isTrue(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/tracking/KnownFindingMatchingAttributesMapperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.tracking.matching.KnownIssueMatchingAttributesMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class KnownFindingMatchingAttributesMapperTests { private final KnownFinding knownFinding = mock(KnownFinding.class); private final KnownIssueMatchingAttributesMapper underTest = new KnownIssueMatchingAttributesMapper(); @BeforeEach void prepare() { when(knownFinding.getId()).thenReturn(UUID.randomUUID()); when(knownFinding.getMessage()).thenReturn("msg"); when(knownFinding.getRuleKey()).thenReturn("ruleKey"); when(knownFinding.getTextRangeWithHash()).thenReturn(new TextRangeWithHash(1, 2, 3, 4, "rangehash")); when(knownFinding.getLineWithHash()).thenReturn(new LineWithHash(1, "linehash")); } @Test void should_delegate_fields_to_server_issue() { assertThat(underTest.getMessage(knownFinding)).isEqualTo(knownFinding.getMessage()); assertThat(underTest.getRuleKey(knownFinding)).isEqualTo(knownFinding.getRuleKey()); assertThat(underTest.getLine(knownFinding)).contains(knownFinding.getLineWithHash().getNumber()); assertThat(underTest.getLineHash(knownFinding)).contains(knownFinding.getLineWithHash().getHash()); assertThat(underTest.getTextRangeHash(knownFinding)).contains(knownFinding.getTextRangeWithHash().getHash()); assertThat(underTest.getServerIssueKey(knownFinding)).isEmpty(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/tracking/LocalOnlyIssueMatchingAttributesMapperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.nio.file.Path; import java.time.Instant; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssueResolution; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.tracking.matching.LocalOnlyIssueMatchingAttributesMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class LocalOnlyIssueMatchingAttributesMapperTests { private final LocalOnlyIssue localOnlyIssue = mock(LocalOnlyIssue.class); private final LocalOnlyIssueMatchingAttributesMapper underTest = new LocalOnlyIssueMatchingAttributesMapper(); @BeforeEach void prepare() { when(localOnlyIssue.getId()).thenReturn(UUID.randomUUID()); when(localOnlyIssue.getMessage()).thenReturn("msg"); when(localOnlyIssue.getResolution()).thenReturn(new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, Instant.now(), null)); when(localOnlyIssue.getRuleKey()).thenReturn("ruleKey"); when(localOnlyIssue.getServerRelativePath()).thenReturn(Path.of("file/path")); when(localOnlyIssue.getTextRangeWithHash()).thenReturn(new TextRangeWithHash(1, 2, 3, 4, "rangehash")); when(localOnlyIssue.getLineWithHash()).thenReturn(new LineWithHash(1, "linehash")); } @Test void should_delegate_fields_to_server_issue() { assertThat(underTest.getMessage(localOnlyIssue)).isEqualTo(localOnlyIssue.getMessage()); assertThat(underTest.getRuleKey(localOnlyIssue)).isEqualTo(localOnlyIssue.getRuleKey()); assertThat(underTest.getLine(localOnlyIssue)).contains(localOnlyIssue.getLineWithHash().getNumber()); assertThat(underTest.getLineHash(localOnlyIssue)).contains(localOnlyIssue.getLineWithHash().getHash()); assertThat(underTest.getTextRangeHash(localOnlyIssue)).contains(localOnlyIssue.getTextRangeWithHash().getHash()); assertThat(underTest.getServerIssueKey(localOnlyIssue)).isEmpty(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/tracking/ServerHotspotMatchingAttributesMapperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import java.nio.file.Path; import java.time.Instant; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.tracking.matching.ServerHotspotMatchingAttributesMapper; import static org.assertj.core.api.Assertions.assertThat; class ServerHotspotMatchingAttributesMapperTests { @Test void should_delegate_fields_to_server_issue() { var creationDate = Instant.now(); var textRange = new TextRangeWithHash(1, 2, 3, 4, "realHash"); var serverHotspot = new ServerHotspot("key", "ruleKey", "message", Path.of("filePath"), textRange, creationDate, HotspotReviewStatus.SAFE, VulnerabilityProbability.LOW, null); var underTest = new ServerHotspotMatchingAttributesMapper(); assertThat(underTest.getServerIssueKey(serverHotspot)).contains("key"); assertThat(underTest.getMessage(serverHotspot)).isEqualTo("message"); assertThat(underTest.getLineHash(serverHotspot)).isEmpty(); assertThat(underTest.getRuleKey(serverHotspot)).isEqualTo("ruleKey"); assertThat(underTest.getLine(serverHotspot)).contains(1); assertThat(underTest.getTextRangeHash(serverHotspot)).contains(textRange.getHash()); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/tracking/ServerIssueMatchingAttributesMapperTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.tracking; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.tracking.matching.ServerIssueMatchingAttributesMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ServerIssueMatchingAttributesMapperTests { private final LineLevelServerIssue serverIssue = mock(LineLevelServerIssue.class); private final ServerIssueMatchingAttributesMapper underTest = new ServerIssueMatchingAttributesMapper(); @BeforeEach void prepare() { when(serverIssue.getLineHash()).thenReturn("blah"); when(serverIssue.isResolved()).thenReturn(true); when(serverIssue.getLine()).thenReturn(22); } @Test void should_delegate_fields_to_server_issue() { assertThat(underTest.getMessage(serverIssue)).isEqualTo(serverIssue.getMessage()); assertThat(underTest.getLineHash(serverIssue)).contains(serverIssue.getLineHash()); assertThat(underTest.getRuleKey(serverIssue)).isEqualTo(serverIssue.getRuleKey()); assertThat(underTest.getLine(serverIssue)).contains(serverIssue.getLine()); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/websocket/SonarCloudWebSocketTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket; import java.io.IOException; import java.net.URI; import java.net.http.WebSocket; import java.nio.channels.UnresolvedAddressException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.http.WebSocketClient; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SonarCloudWebSocketTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private WebSocketClient webSocketClient; private WebSocket mockWebSocket; private CompletableFuture wsFuture; private Consumer serverEventConsumer; private Runnable connectionEndedRunnable; private SonarCloudWebSocket sonarCloudWebSocket; private URI testUri; @BeforeEach void setUp() { webSocketClient = mock(WebSocketClient.class); mockWebSocket = mock(WebSocket.class); wsFuture = new CompletableFuture<>(); serverEventConsumer = mock(Consumer.class); connectionEndedRunnable = mock(Runnable.class); testUri = URI.create("wss://test.example.com/websocket"); } @Test void should_create_websocket_connection_successfully() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); assertThat(sonarCloudWebSocket).isNotNull(); verify(webSocketClient).createWebSocketConnection(eq(testUri), any(Consumer.class), any(Runnable.class)); assertThat(logTester.logs()).anyMatch(log -> log.contains("Creating WebSocket connection to " + testUri)); } @Test void should_handle_connection_failure_with_generic_exception() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.completeExceptionally(new RuntimeException("Generic error")); assertThat(sonarCloudWebSocket).isNotNull(); assertThat(logTester.logs(LogOutput.Level.ERROR)).anyMatch(log -> log.contains("Error while trying to create WebSocket connection for " + testUri)); } @Test void should_close_websocket_connection_with_proper_completion() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.completedFuture(null)); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); // Simulate the WebSocket input being closed by the server BEFORE calling close onClosedCaptor.getValue().run(); // Now call close - it should complete immediately since webSocketInputClosed is already completed sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs()).anyMatch(log -> log.contains("Closing SonarCloud WebSocket connection, reason=Test reason")); assertThat(logTester.logs()).anyMatch(log -> log.contains("Waiting for SonarCloud WebSocket input to be closed...")); assertThat(logTester.logs()).anyMatch(log -> log.contains("SonarCloud WebSocket closed")); } @Test void should_handle_close_execution_exception() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.failedFuture(new RuntimeException("Close failed"))); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs(LogOutput.Level.ERROR)).anyMatch(log -> log.contains("Cannot close the WebSocket output")); } @Test void should_handle_unresolved_address_exception_during_close() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.failedFuture(new UnresolvedAddressException())); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); // Capture the onClosedRunnable callback and complete it to avoid timeout var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); onClosedCaptor.getValue().run(); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs(LogOutput.Level.DEBUG)).anyMatch(log -> log.contains("WebSocket could not be closed gracefully")); } @Test void should_handle_ioexception_with_output_closed_message() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.failedFuture(new IOException("Output closed"))); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); // Capture the onClosedRunnable callback and complete it to avoid timeout var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); onClosedCaptor.getValue().run(); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs(LogOutput.Level.DEBUG)).anyMatch(log -> log.contains("WebSocket could not be closed gracefully")); } @Test void should_handle_ioexception_with_closed_output_message() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.failedFuture(new IOException("closed output"))); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); // Capture the onClosedRunnable callback and complete it to avoid timeout var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); onClosedCaptor.getValue().run(); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs(LogOutput.Level.DEBUG)).anyMatch(log -> log.contains("WebSocket could not be closed gracefully")); } @Test void should_handle_ioexception_with_different_message() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(false); when(mockWebSocket.sendClose(anyInt(), anyString())).thenReturn(CompletableFuture.failedFuture(new IOException("Connection reset"))); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); // Capture the onClosedRunnable callback and complete it to avoid timeout var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); onClosedCaptor.getValue().run(); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket).sendClose(WebSocket.NORMAL_CLOSURE, ""); assertThat(logTester.logs(LogOutput.Level.ERROR)).anyMatch(log -> log.contains("Cannot close the WebSocket output")); } @Test void should_handle_already_closed_websocket() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isOutputClosed()).thenReturn(true); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); sonarCloudWebSocket.close("Test reason"); verify(mockWebSocket, never()).sendClose(anyInt(), anyString()); } @Test void should_handle_failed_websocket_future() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.completeExceptionally(new RuntimeException("Connection failed")); sonarCloudWebSocket.close("Test reason"); assertThat(logTester.logs()).anyMatch(log -> log.contains("WebSocket connection was already closed, skipping close operation")); } @Test void should_handle_pending_websocket_future() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))) .thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); sonarCloudWebSocket.close("Test reason"); assertThat(logTester.logs()).anyMatch(log -> log.contains("WebSocket connection was still pending, cancelled")); } @Test void should_check_if_websocket_is_open() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isInputClosed()).thenReturn(false); when(mockWebSocket.isOutputClosed()).thenReturn(false); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); assertThat(sonarCloudWebSocket.isOpen()).isTrue(); } @Test void should_return_false_when_websocket_is_closed() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); when(mockWebSocket.isInputClosed()).thenReturn(true); when(mockWebSocket.isOutputClosed()).thenReturn(false); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); assertThat(sonarCloudWebSocket.isOpen()).isFalse(); } @Test void should_return_false_when_websocket_future_is_not_done() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); assertThat(sonarCloudWebSocket.isOpen()).isFalse(); } @Test void should_return_false_when_websocket_future_failed() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.completeExceptionally(new RuntimeException("Connection failed")); assertThat(sonarCloudWebSocket.isOpen()).isFalse(); } @Test void should_return_false_when_websocket_future_is_cancelled() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.cancel(true); assertThat(sonarCloudWebSocket.isOpen()).isFalse(); } @Test void should_handle_connection_ended_callback() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); // Simulate connection ended onClosedCaptor.getValue().run(); verify(connectionEndedRunnable).run(); } @Test void should_not_call_connection_ended_callback_when_closing_initiated() { when(webSocketClient.createWebSocketConnection(any(URI.class), any(Consumer.class), any(Runnable.class))).thenReturn(wsFuture); when(mockWebSocket.sendText(anyString(), eq(true))).thenReturn(CompletableFuture.completedFuture(null)); sonarCloudWebSocket = SonarCloudWebSocket.create(testUri, webSocketClient, serverEventConsumer, connectionEndedRunnable); wsFuture.complete(mockWebSocket); // Close the connection first sonarCloudWebSocket.close("Test reason"); var onClosedCaptor = ArgumentCaptor.forClass(Runnable.class); verify(webSocketClient).createWebSocketConnection(any(URI.class), any(Consumer.class), onClosedCaptor.capture()); // Simulate connection ended after closing was initiated onClosedCaptor.getValue().run(); // Should not call the callback since closing was initiated verify(connectionEndedRunnable, never()).run(); } } ================================================ FILE: backend/core/src/test/java/org/sonarsource/sonarlint/core/websocket/parsing/SmartNotificationEventParserTests.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.websocket.parsing; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class SmartNotificationEventParserTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private SmartNotificationEventParser smartNotificationEventParser; @Test void should_parse_valid_json_date() { smartNotificationEventParser = new SmartNotificationEventParser("QA"); var jsonData = "{\"message\": \"msg\", \"link\": \"lnk\", \"project\": \"projectKey\", \"date\": \"2023-07-19T15:08:01+0000\"}"; var optionalEvent = smartNotificationEventParser.parse(jsonData); assertThat(optionalEvent).isPresent(); var event = optionalEvent.get(); assertThat(event.category()).isEqualTo("QA"); assertThat(event.date()).isEqualTo("2023-07-19T15:08:01+0000"); assertThat(event.message()).isEqualTo("msg"); assertThat(event.project()).isEqualTo("projectKey"); assertThat(event.link()).isEqualTo("lnk"); } @Test void should_not_parse_invalid_json_date() { smartNotificationEventParser = new SmartNotificationEventParser("QA"); var jsonData = "{\"invalid\": \"msg\", \"link\": \"lnk\", \"project\": \"projectKey\", \"date\": \"2023-07-19T15:08:01+0000\"}"; var optionalEvent = smartNotificationEventParser.parse(jsonData); assertThat(optionalEvent).isEmpty(); } } ================================================ FILE: backend/core/src/test/java/testutils/LocalOnlyIssueFixtures.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.UUID; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssueResolution; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class LocalOnlyIssueFixtures { public static LocalOnlyIssue aLocalOnlyIssueResolvedWithoutTextAndLineRange() { return new LocalOnlyIssue( UUID.randomUUID(), Path.of("file/path"), null, null, "ruleKey", "message", new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, Instant.now().truncatedTo(ChronoUnit.MILLIS), "comment") ); } public static LocalOnlyIssue aLocalOnlyIssueResolved() { return aLocalOnlyIssueResolved(UUID.randomUUID()); } public static LocalOnlyIssue aLocalOnlyIssueResolved(Instant resolutionDate) { return aLocalOnlyIssueResolved(UUID.randomUUID(), resolutionDate); } public static LocalOnlyIssue aLocalOnlyIssueResolved(UUID id) { return aLocalOnlyIssueResolved(id, Instant.now()); } public static LocalOnlyIssue aLocalOnlyIssueResolved(UUID id, Instant resolutionDate) { return new LocalOnlyIssue( id, Path.of("file/path"), new TextRangeWithHash(1, 2, 3, 4, "ab12"), new LineWithHash(1, "linehash"), "ruleKey", "message", new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, resolutionDate.truncatedTo(ChronoUnit.MILLIS), "comment") ); } } ================================================ FILE: backend/core/src/test/java/testutils/TakeThreadDumpAfter.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface TakeThreadDumpAfter { /** Set to zero to disable timeout **/ int seconds(); /** Set to true if the test is expected to timeout and this is OK with you. This is mostly for self-testing the extension. **/ boolean expectTimeout() default false; } ================================================ FILE: backend/core/src/test/java/testutils/TestUtils.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.lang.management.ManagementFactory; public class TestUtils { private static String generateThreadDump() { final var dump = new StringBuilder(); final var threadMXBean = ManagementFactory.getThreadMXBean(); final var threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100); for (var threadInfo : threadInfos) { dump.append('"'); dump.append(threadInfo.getThreadName()); dump.append("\" "); final var state = threadInfo.getThreadState(); dump.append("\n java.lang.Thread.State: "); dump.append(state); final var stackTraceElements = threadInfo.getStackTrace(); for (final var stackTraceElement : stackTraceElements) { dump.append("\n at "); dump.append(stackTraceElement); } dump.append("\n\n"); } return dump.toString(); } public static void printThreadDump() { System.out.println(generateThreadDump()); } } ================================================ FILE: backend/core/src/test/java/testutils/ThreadDumpExtension.java ================================================ /* * SonarLint Core - Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import java.lang.reflect.Method; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; import static testutils.TestUtils.printThreadDump; public class ThreadDumpExtension implements InvocationInterceptor { private static final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); @Override public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { var timeout = invocationContext.getExecutable().getAnnotation(TakeThreadDumpAfter.class); var clazz = invocationContext.getExecutable().getDeclaringClass(); while (timeout == null && clazz != Object.class) { timeout = clazz.getAnnotation(TakeThreadDumpAfter.class); clazz = clazz.getSuperclass(); } if (timeout == null || timeout.seconds() <= 0) { invocation.proceed(); return; } var seconds = timeout.seconds(); var caller = Thread.currentThread(); var timedOut = new AtomicBoolean(); Future future = exec.schedule(() -> { System.out.println("**** TIMEOUT ERROR: TEST EXCEEDED " + seconds + " SECONDS ****"); printThreadDump(); timedOut.set(true); caller.interrupt(); return null; }, seconds, TimeUnit.SECONDS); Exception caught = null; try { invocation.proceed(); } catch (Exception ex) { caught = ex; } finally { future.cancel(true); if (timedOut.get()) { if (!timeout.expectTimeout()) { Exception ex = new TimeoutException("Test exceeded timeout of " + seconds + " seconds"); if (caught != null) { ex.addSuppressed(caught); } throw ex; } } else if (caught != null) { throw caught; } else if (timeout.expectTimeout()) { throw new RuntimeException("Test expected to timeout at " + seconds + " but didn't"); } } } } ================================================ FILE: backend/core/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/core/src/test/resources/ondemand/sonar-cpp-corrupt-plugin.jar.asc ================================================ -----BEG ================================================ FILE: backend/core/src/test/resources/ondemand/sonar-cpp-plugin.jar.asc ================================================ -----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEE5uYX+edGKHKP3xYibMoJTbbStoAFAmmljsUACgkQbMoJTbbS toDAtQ//ZLTHCRJ1EImtlEnYybFcd+a1Al42FCI7XwKGamwPxX1W5URp8z2SY8CC P4Z1CDsh2U8rCohGqaLcd6xaoGDdMJZUxU2D/64VbBnIc0fiYzyDVt8tc5QybYGI T3n3t5JfZG/BEXKGg/c3Rzk95H2ArmV6i7S/6IW2TjuBd8foROonln+5SpQRDFWN fkP3Av6JK/xK3FoDTnhidbJhh3irWTRScbXVx7GRT+HyNKlIZzNz6OEfrpMu0mGk j1xeuoa+082sqfq9vc7crF+7mEhSLX7N1iEqgBFtbUj7rTJAN90HXw7TvzP6MxOz Iag29dh3NO8tAjtykSMWtLBd7xilWNQA3v4gQSdg0u14vU6JVuwO3UXrXg4QZr6K VBzXujSxeII2kIHLeK9PyKvcqsMBdCq9Fal+/JO8Ev6YERuFT1qGdSWqSAW7CdFl LVDNM+PVZ7cpZhAuSMrbtERhfoADLODeFCBbAorpp/00RsD8uPOgrsLVqspk19uT zLPfpT1P8B8vWZlAqfx4nVpJ1QCD6HcrsFko1jqBATQB0C5wdvLpAntvLOOw2wy2 8OwmisNNl19i0Hm48ohkTm8eFYQUcv/uC9sUPdckTaZOG5ughYqUZNJzASWoz2Ma 8ngjnG64IBA0jvnY6y5AKA/dIlNc+MMwAJ2RYh0A8IowAz8DyQI= =kU4O -----END PGP SIGNATURE----- ================================================ FILE: backend/core/src/test/resources/ondemand/sonar-cpp-unknownkey-plugin.jar.asc ================================================ -----BEGIN PGP SIGNATURE----- iQFFBAABCgAvFiEEXvC7OWK1m82TnVrOUsDmg8MZOC0FAmm7/X8RHHRlc3RAZXhh bXBsZS5jb20ACgkQUsDmg8MZOC3f/Qf5AWtIKjebbkio1QBHO1z7CrjOpHyjtHzO Zpf7EjicM4Yfq7zUzEOeRy9DAttBtkSat4cxUgs4IHg2FxFCwvq49/+/f4AJT9/S CWQnaCI9tA+3aXJ8Vz5VDLNbZFblmaiWcnDye/39Sqv9saxxyf4iOzg/uKFrQ25B ebOrV2wFp+GRp1pzPhZEQv9/pUwzgC5vfZIfOe+d+tTCLzZ4IgIKeSmVUA+XYogi mP+rKeOzj3vicxl6+8eyTEoShY1Y08isLrqhsWbUxFXl0LM17uTc9VMw/jYBDFp2 hJw6ASpJtFjfm3NmNffqRgtX/k81YYz4HzqfhaKzkVh1bR5vUM6MBg== =ugwf -----END PGP SIGNATURE----- ================================================ FILE: backend/core/src/test/resources/ondemand/sonarsource-public.key ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGmTDYEBEADBL3EBQ1xSS+omyRmmCcE+IgRz/i2aSP4ZD0W5xawminV0EfNf N/AEV0aBoi2yzQqPm5rZODqvOpdP4t3rJKAvygvkiMHornY97cwQiJ6T4iiz04yk l41oO513kZV2BvBclU4WrwDGDoTBH6KPn52r2k8eGtnDl1QKfGbK9ey8/Hs8ySCT zOaWZ+mRqF6SrEmG8OzrP+u1O2MwqC+KNnywpl1OFeBcpBAhzDgMd3ZKe3B69w1w wXr9KnmpYkQ9pe90wjWI2xL8JFePTymq26nd9hMhiuPXbIEWcJs6gGACEJOgdOJM /f5LTu7Vm3eOj8wmmCeXIc9QJT8LNm29ie+PDH1qnQLpW5edEbV4wJY9WdmQ7LeV bKJU29vGsE/I5GkB6PE+0RdNx0hggKzV+1opeoZw4Fl4RsF35fUtpv5d2sb86u8g MUY38dSgvW8cHbqkXAdusN3ys9PiTRdZd6lcZX7U8TicQ2sVkyXJF1GjPSFG6TeF XaTN/jh6P4EoLAlO5x6KC9Q43O2oYuvFQtDSkqjqG4JwVPEWOOo51LNi2cJrEJDM nTKCh3hsh3dPEHcHJwF4v9vA1kiNXVVBHw7yNi/HScqgNoSGbOMDg8OPyPuLNUw3 helyJOfStvAk7mdllhF1g+8mGAfwezXFTnQG84LU5bpUMBmirUst6rElpQARAQAB tBxUZXN0IFVzZXIgPHRlc3RAZXhhbXBsZS5jb20+iQJOBBMBCgA4FiEE5uYX+edG KHKP3xYibMoJTbbStoAFAmmTDYECGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA CgkQbMoJTbbStoBRfhAAtQYbgRGZnpBjtF/U0ZZYNwlX0vaDU2vddL8Bh0pvHlJH 6KfQZPUzW6KsxoNQzU2VI4d4GgrA/pAcWIs3yczg5ZxPWQBilTD1s8Djbjl5iGTW BISGoafYaUSyRKc10rtKKJXxYdxzGFlkgrPta7F6zEHFxKJZm0O9RpWpRG4nOGEN OD3DmrF17qNkdKebsHU0eq0Zf0vNEl848Ja9KfUGGv4lrf5mM4n4FY8LwxL9cCZR vCZlHkQMDFH8d6sgDYK4jP3ebJI+83ckk6L9z05l04AgJuLoIL19UkWFgLCcSUjW Lo6QXIvmTgEI81wfihfrn57aGD/d057OKri2wg717G6jX7bnfc5/fFYmvRJGtTNo Q9DSEyVePzb0ytCNNL6mgMQpZCA7NIOdBQK7TiKwFEDxHCpoij2cCGJtijUlZNS0 v9fFG5obVG5PUGcwj78gfaMys+rlU0P1+4pT45bQprz/DXaCQJbgP2qoIlVxf4T/ M3/VHkYZ2bLFUHsIpP3zH+k6zHOZSquf0zHl1e10Lz+4bPTGpwaMpJ84b6qmJ3en WqzbSvw0RgVmX3A8/3vKNd0w+fDY5GAk6oh4R5aEig14fhkO1FE+40w9M4L9jhsK FYm2pEQxniUUMAbYQfEjRDonK71ZGXCgAY+lHNEtRelucFPHwOE12cezUHQ6TYk= =buVr -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: backend/http/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-http SonarLint Core - HTTP HTTP related code used by SonarLint com.google.code.findbugs jsr305 provided jakarta.inject jakarta.inject-api jakarta.annotation jakarta.annotation-api javax.annotation javax.annotation-api ${project.groupId} sonarlint-commons ${project.version} org.apache.httpcomponents.client5 httpclient5 org.slf4j slf4j-api io.github.hakky54 ayza org.slf4j slf4j-api com.google.guava guava org.apache.commons commons-lang3 org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test org.wiremock wiremock-jetty12 test ch.qos.logback logback-classic test conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ApacheHttpClientAdapter.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.io.InterruptedIOException; import java.net.URISyntaxException; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import javax.annotation.Nullable; import org.apache.hc.client5.http.async.methods.AbstractCharResponseConsumer; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; import org.apache.hc.core5.util.Timeout; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; class ApacheHttpClientAdapter implements HttpClient { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String X_API_KEY_HEADER = "x-api-key"; private static final Timeout STREAM_CONNECTION_REQUEST_TIMEOUT = Timeout.ofSeconds(10); private static final Timeout STREAM_CONNECTION_TIMEOUT = Timeout.ofMinutes(1); private final CloseableHttpAsyncClient apacheClient; @Nullable private final String usernameOrToken; @Nullable private final String password; private final boolean shouldUseBearer; @Nullable private final String xApiKey; private final boolean withRetries; private boolean connected = false; private ApacheHttpClientAdapter(CloseableHttpAsyncClient apacheClient, @Nullable String usernameOrToken, @Nullable String password, boolean shouldUseBearer, @Nullable String xApiKey, boolean withRetries) { this.apacheClient = apacheClient; this.usernameOrToken = usernameOrToken; this.password = password; this.shouldUseBearer = shouldUseBearer; this.xApiKey = xApiKey; this.withRetries = withRetries; } @Override public Response post(String url, String contentType, String bodyContent) { return waitFor(postAsync(url, contentType, bodyContent)); } @Override public CompletableFuture postAsync(String url, String contentType, String body) { var request = SimpleRequestBuilder.post(url) .setBody(body, ContentType.parse(contentType)) .build(); return executeAsync(request); } @Override public Response get(String url) { return waitFor(getAsync(url)); } @Override public CompletableFuture getAsync(String url) { return executeAsync(SimpleRequestBuilder.get(url).build()); } @Override public CompletableFuture getAsyncAnonymous(String url) { return executeAsyncAnonymous(SimpleRequestBuilder.get(url).build()); } @Override public CompletableFuture deleteAsync(String url, String contentType, String body) { var httpRequest = SimpleRequestBuilder .delete(url) .setBody(body, ContentType.parse(contentType)) .build(); return executeAsync(httpRequest); } private static Response waitFor(CompletableFuture f) { return f.join(); } @Override public AsyncRequest getEventStream(String url, HttpConnectionListener connectionListener, Consumer messageConsumer) { var request = SimpleRequestBuilder.get(url).build(); request.setConfig(RequestConfig.custom() .setConnectionRequestTimeout(STREAM_CONNECTION_REQUEST_TIMEOUT) .setConnectTimeout(STREAM_CONNECTION_TIMEOUT) .setResponseTimeout(Timeout.ZERO_MILLISECONDS) .build()); setAuthHeader(request); request.setHeader("Accept", "text/event-stream"); connected = false; var cancelled = new AtomicBoolean(); var httpFuture = apacheClient.execute(new BasicRequestProducer(request, null), new AbstractCharResponseConsumer<>() { @Override public void releaseResources() { // should we close something ? } @Override protected int capacityIncrement() { return Integer.MAX_VALUE; } @Override protected void data(CharBuffer src, boolean endOfStream) { if (cancelled.get()) { throw new CancellationException(); } if (connected) { messageConsumer.accept(src.toString()); } else { var possiblyErrorMessage = src.toString(); if (!possiblyErrorMessage.isEmpty()) { LOG.debug("Received event-stream data while not connected: " + possiblyErrorMessage); } } } @Override protected void start(HttpResponse httpResponse, ContentType contentType) { if (httpResponse.getCode() < 200 || httpResponse.getCode() >= 300) { connectionListener.onError(httpResponse.getCode()); } else { connected = true; connectionListener.onConnected(); } } @Override protected Object buildResult() { return null; } @Override public void failed(Exception cause) { if (cause instanceof CancellationException || cause instanceof InterruptedIOException) { return; } LOG.error("Stream failed", cause); } }, new FutureCallback<>() { @Override public void completed(Object result) { if (connected) { connectionListener.onClosed(); } } @Override public void failed(Exception ex) { if (connected) { // called when disconnected from server connectionListener.onClosed(); } else { connectionListener.onError(null); } } @Override public void cancelled() { cancelled.set(true); LOG.debug("Stream has been cancelled"); } }); return new HttpAsyncRequest(httpFuture); } private void setAuthHeader(SimpleHttpRequest request) { if (usernameOrToken != null) { if (shouldUseBearer) { request.setHeader(AUTHORIZATION_HEADER, bearer(usernameOrToken)); } else { request.setHeader(AUTHORIZATION_HEADER, basic(usernameOrToken, Objects.requireNonNullElse(password, ""))); } } else if (xApiKey != null) { request.setHeader(X_API_KEY_HEADER, xApiKey); } } private class CompletableFutureWrappingFuture extends CompletableFuture { private final Future wrapped; private CompletableFutureWrappingFuture(SimpleHttpRequest httpRequest) { var callingThreadLogOutput = SonarLintLogger.get().getTargetForCopy(); var context = new HttpClientContext(); context.setAttribute(ContextAttributes.RETRIES_ENABLED, withRetries); this.wrapped = apacheClient.execute(httpRequest, context, new FutureCallback<>() { @Override public void completed(SimpleHttpResponse result) { SonarLintLogger.get().setTarget(callingThreadLogOutput); // getRequestUri may be relative, so we prefer getUri try { var uri = httpRequest.getUri().toString(); CompletableFutureWrappingFuture.this.completeAsync(() -> { SonarLintLogger.get().setTarget(callingThreadLogOutput); return new ApacheHttpResponse(uri, result); }); } catch (URISyntaxException e) { CompletableFutureWrappingFuture.this.completeAsync(() -> { SonarLintLogger.get().setTarget(callingThreadLogOutput); return new ApacheHttpResponse(httpRequest.getRequestUri(), result); }); } } @Override public void failed(Exception ex) { SonarLintLogger.get().setTarget(callingThreadLogOutput); LOG.debug("Request failed", ex); CompletableFutureWrappingFuture.this.completeExceptionally(ex); } @Override public void cancelled() { SonarLintLogger.get().setTarget(callingThreadLogOutput); LOG.debug("Request cancelled"); CompletableFutureWrappingFuture.this.cancel(); } }); } private void cancel() { super.cancel(true); } @Override public boolean cancel(boolean mayInterruptIfRunning) { return wrapped.cancel(mayInterruptIfRunning); } } private CompletableFuture executeAsync(SimpleHttpRequest httpRequest) { try { setAuthHeader(httpRequest); return new CompletableFutureWrappingFuture(httpRequest); } catch (Exception e) { throw new IllegalStateException("Unable to execute request: " + e.getMessage(), e); } } private CompletableFuture executeAsyncAnonymous(SimpleHttpRequest httpRequest) { try { return new CompletableFutureWrappingFuture(httpRequest); } catch (Exception e) { throw new IllegalStateException("Unable to execute request: " + e.getMessage(), e); } } private static String basic(String username, String password) { var usernameAndPassword = String.format("%s:%s", username, password); var encoded = Base64.getEncoder().encodeToString(usernameAndPassword.getBytes(StandardCharsets.UTF_8)); return String.format("Basic %s", encoded); } private static String bearer(String token) { return String.format("Bearer %s", token); } public static class HttpAsyncRequest implements AsyncRequest { private final Future response; private HttpAsyncRequest(Future response) { this.response = response; } @Override public void cancel() { try { response.cancel(true); } catch (Exception e) { // ignore errors } } } public static Builder builder() { return new Builder(); } public static final class Builder { private CloseableHttpAsyncClient apacheClient; @Nullable private String usernameOrToken; @Nullable private String password; private boolean shouldUseBearer = false; @Nullable private String xApiKey; private boolean withRetries = false; public Builder withInnerClient(CloseableHttpAsyncClient apacheClient) { this.apacheClient = apacheClient; return this; } public Builder withUserNamePassword(String username, @Nullable String password) { this.usernameOrToken = username; this.password = password; return this; } public Builder withToken(String token) { this.usernameOrToken = token; return this; } public Builder useBearer(boolean shouldUseBearer) { this.shouldUseBearer = shouldUseBearer; return this; } public Builder withXApiKey(String xApiKey) { this.xApiKey = xApiKey; return this; } public Builder withRetries() { this.withRetries = true; return this; } ApacheHttpClientAdapter build() { if (apacheClient == null) { throw new IllegalStateException("Required an Apache HTTP client to wrap."); } return new ApacheHttpClientAdapter(apacheClient, usernameOrToken, password, shouldUseBearer, xApiKey, withRetries); } } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ApacheHttpResponse.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.io.ByteArrayInputStream; import java.io.InputStream; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; class ApacheHttpResponse implements HttpClient.Response { private final String requestUrl; private final SimpleHttpResponse response; public ApacheHttpResponse(String requestUrl, SimpleHttpResponse response) { this.requestUrl = requestUrl; this.response = response; } @Override public int code() { return response.getCode(); } @Override public String bodyAsString() { return response.getBodyText(); } @Override public InputStream bodyAsStream() { if (response.getBodyBytes() == null) { return new ByteArrayInputStream(new byte[0]); } return new ByteArrayInputStream(response.getBodyBytes()); } @Override public void close() { // nothing to do } @Override public String url() { return requestUrl; } @Override public String toString() { return response.toString(); } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ContextAttributes.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; public class ContextAttributes { public static final String RETRIES_ENABLED = "retries.enabled"; private ContextAttributes() { } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/HttpClient.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.io.Closeable; import java.io.InputStream; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public interface HttpClient { String JSON_CONTENT_TYPE = "application/json; charset=utf-8"; String FORM_URL_ENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"; interface Response extends Closeable { int code(); default boolean isSuccessful() { return code() >= 200 && code() < 300; } String bodyAsString(); InputStream bodyAsStream(); /** * Only runtime exception */ @Override void close(); String url(); } Response get(String url); CompletableFuture getAsync(String url); CompletableFuture getAsyncAnonymous(String url); AsyncRequest getEventStream(String url, HttpConnectionListener connectionListener, Consumer messageConsumer); Response post(String url, String contentType, String body); CompletableFuture postAsync(String url, String contentType, String body); CompletableFuture deleteAsync(String url, String contentType, String body); interface AsyncRequest { void cancel(); } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/HttpClientProvider.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PreDestroy; import java.net.ProxySelector; import java.nio.file.Files; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import nl.altindag.ssl.SSLFactory; import nl.altindag.ssl.model.TrustManagerParameters; import org.apache.commons.lang3.SystemUtils; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClients; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.http.ssl.SslConfig; import static org.sonarsource.sonarlint.core.http.ThreadFactories.threadWithNamePrefix; public class HttpClientProvider { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final int DEFAULT_MAX_RETRIES = 2; private static final int DEFAULT_RETRY_INTERVAL = 3; private final CloseableHttpAsyncClient sharedClient; private final ExecutorService webSocketThreadPool; private final String userAgent; /** * Return an {@link HttpClientProvider} made for testing, with a dummy user agent, and basic configuration regarding proxy/SSL */ public static HttpClientProvider forTesting() { return new HttpClientProvider("SonarLint tests", new HttpConfig(new SslConfig(null, null), null, null, null, null), null, ProxySelector.getDefault(), new BasicCredentialsProvider()); } public HttpClientProvider(String userAgent, HttpConfig httpConfig, @Nullable Predicate trustManagerParametersPredicate, ProxySelector proxySelector, CredentialsProvider proxyCredentialsProvider) { this.userAgent = userAgent; this.webSocketThreadPool = FailSafeExecutors.newCachedThreadPool(threadWithNamePrefix("sonarcloud-websocket-")); var maxRetries = Integer.parseInt(System.getProperty("sonarlint.http.max.retries", String.valueOf(DEFAULT_MAX_RETRIES))); var retryInterval = Integer.parseInt(System.getProperty("sonarlint.http.retry.interval.seconds", String.valueOf(DEFAULT_RETRY_INTERVAL))); sharedClient = buildSharedClient(userAgent, httpConfig, trustManagerParametersPredicate, proxySelector, proxyCredentialsProvider, maxRetries, retryInterval); sharedClient.start(); } private static CloseableHttpAsyncClient buildSharedClient(String userAgent, HttpConfig httpConfig, @Nullable Predicate trustManagerParametersPredicate, ProxySelector proxySelector, CredentialsProvider proxyCredentialsProvider, int maxRetries, int retryInterval) { var asyncConnectionManager = PoolingAsyncClientConnectionManagerBuilder.create() .setTlsStrategy(new DefaultClientTlsStrategy(configureSsl(httpConfig.sslConfig(), trustManagerParametersPredicate))) .setDefaultTlsConfig(TlsConfig.custom() // Force HTTP/1 since we know SQ/SC don't support HTTP/2 ATM .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1) .build()) .setDefaultConnectionConfig(buildConnectionConfig(httpConfig.connectTimeout(), httpConfig.socketTimeout())) .build(); var routePlanner = new SystemDefaultRoutePlanner(proxySelector); return HttpAsyncClients.custom() .setConnectionManager(asyncConnectionManager) .addResponseInterceptorFirst(new RedirectInterceptor()) .setUserAgent(userAgent) // proxy settings .setRoutePlanner(routePlanner) .setDefaultCredentialsProvider(proxyCredentialsProvider) .setDefaultRequestConfig(buildRequestConfig(httpConfig.connectionRequestTimeout(), httpConfig.responseTimeout())) .setRetryStrategy(new RetryOnDemandStrategy(maxRetries, TimeValue.ofSeconds(retryInterval))) .build(); } private static SSLContext configureSsl(SslConfig sslConfig, @Nullable Predicate trustManagerParametersPredicate) { var sslFactoryBuilder = SSLFactory.builder() .withDefaultTrustMaterial(); // SLCORE-686; SLCORE-669 if (isNotWindows()) { sslFactoryBuilder.withSystemTrustMaterial(); } var keyStore = sslConfig.getKeyStore(); if (keyStore != null && Files.exists(keyStore.getPath())) { try { sslFactoryBuilder.withIdentityMaterial(keyStore.getPath(), keyStore.getKeyStorePassword().toCharArray(), keyStore.getKeyStoreType()); } catch (Exception e) { LOG.warn("Unable to load key store from '{}', it will be ignored: {}", keyStore.getPath(), e.getMessage()); } } var trustStore = sslConfig.getTrustStore(); if (trustStore != null) { try { sslFactoryBuilder.withInflatableTrustMaterial(trustStore.getPath(), trustStore.getKeyStorePassword().toCharArray(), trustStore.getKeyStoreType(), trustManagerParametersPredicate); } catch (Exception e) { LOG.warn("Unable to load trust store from '{}', it will be ignored: {}", trustStore.getPath(), e.getMessage()); } } return sslFactoryBuilder.build().getSslContext(); } private static boolean isNotWindows() { return !SystemUtils.IS_OS_WINDOWS; } private static ConnectionConfig buildConnectionConfig(@Nullable Timeout connectTimeout, @Nullable Timeout socketTimeout) { var connectionConfig = ConnectionConfig.custom(); if (connectTimeout != null) { connectionConfig.setConnectTimeout(connectTimeout); } if (socketTimeout != null) { connectionConfig.setSocketTimeout(socketTimeout); } return connectionConfig.build(); } private static RequestConfig buildRequestConfig(@Nullable Timeout connectionRequestTimeout, @Nullable Timeout responseTimeout) { var requestConfig = RequestConfig.custom() .setContentCompressionEnabled(false); if (connectionRequestTimeout != null) { requestConfig.setConnectionRequestTimeout(connectionRequestTimeout); } if (responseTimeout != null) { requestConfig.setResponseTimeout(responseTimeout); } return requestConfig.build(); } public HttpClient getHttpClientWithoutAuth() { return ApacheHttpClientAdapter.builder() .withInnerClient(sharedClient) .build(); } public HttpClient getHttpClientWithPreemptiveAuth(String username, @Nullable String password) { return ApacheHttpClientAdapter.builder() .withInnerClient(sharedClient) .withUserNamePassword(username, password) .build(); } public HttpClient getHttpClientWithPreemptiveAuth(String token, boolean shouldUseBearer) { return ApacheHttpClientAdapter.builder() .withInnerClient(sharedClient) .withToken(token) .useBearer(shouldUseBearer) .build(); } public HttpClient getHttpClientWithXApiKeyAndRetries(String xApiKey) { return ApacheHttpClientAdapter.builder() .withInnerClient(sharedClient) .withXApiKey(xApiKey) .withRetries() .build(); } public WebSocketClient getWebSocketClient(String token) { return new WebSocketClient(userAgent, token, webSocketThreadPool); } @PreDestroy public void close() { sharedClient.close(CloseMode.IMMEDIATE); if (!MoreExecutors.shutdownAndAwaitTermination(webSocketThreadPool, 1, TimeUnit.SECONDS)) { LOG.warn("Unable to stop web socket executor service in a timely manner"); } } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/HttpConfig.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import javax.annotation.Nullable; import org.apache.hc.core5.util.Timeout; import org.sonarsource.sonarlint.core.http.ssl.SslConfig; public record HttpConfig(SslConfig sslConfig, @Nullable Timeout connectTimeout, @Nullable Timeout socketTimeout, @Nullable Timeout connectionRequestTimeout, @Nullable Timeout responseTimeout) { private static final Timeout DEFAULT_CONNECT_TIMEOUT = Timeout.ofSeconds(60); private static final Timeout DEFAULT_RESPONSE_TIMEOUT = Timeout.ofMinutes(10); @Override public Timeout connectionRequestTimeout() { if (connectionRequestTimeout == null) { return DEFAULT_CONNECT_TIMEOUT; } return connectionRequestTimeout; } @Override public Timeout responseTimeout() { if (responseTimeout == null) { return DEFAULT_RESPONSE_TIMEOUT; } return responseTimeout; } @Override public Timeout connectTimeout() { if (connectTimeout == null) { return DEFAULT_CONNECT_TIMEOUT; } return connectTimeout; } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/HttpConnectionListener.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import javax.annotation.Nullable; public interface HttpConnectionListener { /** * Should be called when the request returns status >= 200 and < 300. */ void onConnected(); /** * Should be called when the request returns status < 200 or >= 300, or another error occurs. No need to call {@link #onClosed()} after that. * @param responseCode the HTTP status response, or null for other error types (e.g. timeout) */ void onError(@Nullable Integer responseCode); /** * Should be called when the connection is closed, only after it was successfully established (ie after {@link #onConnected()} was called) */ void onClosed(); } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/RedirectInterceptor.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpResponseInterceptor; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; class RedirectInterceptor implements HttpResponseInterceptor { @Override public void process(HttpResponse response, EntityDetails entity, HttpContext context) { alterResponseCodeIfNeeded(context, response); } private static void alterResponseCodeIfNeeded(HttpContext context, HttpResponse response) { if (isPost(context)) { // Apache handles some redirect statuses by transforming the POST into a GET // we force a different status to keep the request a POST var code = response.getCode(); if (code == HttpStatus.SC_MOVED_PERMANENTLY) { response.setCode(HttpStatus.SC_PERMANENT_REDIRECT); } else if (code == HttpStatus.SC_MOVED_TEMPORARILY || code == HttpStatus.SC_SEE_OTHER) { response.setCode(HttpStatus.SC_TEMPORARY_REDIRECT); } } } private static boolean isPost(HttpContext context) { var httpCoreContext = HttpCoreContext.cast(context); var request = httpCoreContext.getRequest(); return request != null && Method.POST.isSame(request.getMethod()); } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/RetryOnDemandStrategy.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.io.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.NoRouteToHostException; import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; import javax.net.ssl.SSLException; import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.TimeValue; public class RetryOnDemandStrategy extends DefaultHttpRequestRetryStrategy { private static final List> SAME_AS_DEFAULT = Arrays.asList( InterruptedIOException.class, UnknownHostException.class, ConnectException.class, ConnectionClosedException.class, NoRouteToHostException.class, SSLException.class); public RetryOnDemandStrategy(int maxRetries, TimeValue defaultRetryInterval) { super(maxRetries, defaultRetryInterval, SAME_AS_DEFAULT, Arrays.asList( HttpStatus.SC_TOO_MANY_REQUESTS, HttpStatus.SC_INTERNAL_SERVER_ERROR, HttpStatus.SC_SERVICE_UNAVAILABLE)); } @Override public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) { return areRetriesEnabled(context) && super.retryRequest(response, execCount, context); } private static boolean areRetriesEnabled(HttpContext context) { var retriesEnabledFlag = context.getAttribute(ContextAttributes.RETRIES_ENABLED); return Boolean.TRUE.equals(retriesEnabledFlag); } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ThreadFactories.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class ThreadFactories { private ThreadFactories() { } public static ThreadFactory threadWithNamePrefix(String namePrefix) { return new ThreadFactoryWithNamePrefix(namePrefix); } private static final class ThreadFactoryWithNamePrefix implements ThreadFactory { private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(); ThreadFactoryWithNamePrefix(String prefix) { this.namePrefix = prefix; } @Override public Thread newThread(Runnable r) { String name = namePrefix + nextId.getAndIncrement(); return new Thread(null, r, name, 0, false); } } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/WebSocketClient.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.net.URI; import java.net.http.HttpClient; import java.net.http.WebSocket; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class WebSocketClient { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; private static final String USER_AGENT_HEADER_NAME = "User-Agent"; private final String userAgent; @Nullable private final String token; private final HttpClient httpClient; WebSocketClient(String userAgent, @Nullable String token, ExecutorService executor) { this.userAgent = userAgent; this.token = token; this.httpClient = HttpClient .newBuilder() // Don't use the default thread pool as it won't allow inheriting thread local variables .executor(executor) .build(); } public CompletableFuture createWebSocketConnection(@Nullable URI uri, Consumer messageConsumer, Runnable onClosedRunnable) { // Validate URI before attempting connection if (uri == null || (!"ws".equals(uri.getScheme()) && !"wss".equals(uri.getScheme()))) { var future = new CompletableFuture(); future.completeExceptionally(new IllegalArgumentException("WebSocket URI must use 'ws' or 'wss' scheme: " + uri)); return future; } // TODO handle handshake or other errors var currentThreadOutput = SonarLintLogger.get().getTargetForCopy(); return httpClient .newWebSocketBuilder() .header(AUTHORIZATION_HEADER_NAME, "Bearer " + token) .header(USER_AGENT_HEADER_NAME, userAgent) .buildAsync(uri, new MessageConsumerWrapper(messageConsumer, onClosedRunnable, currentThreadOutput)); } private record MessageConsumerWrapper(Consumer messageConsumer, Runnable onWebSocketInputClosedRunnable, @Nullable LogOutput currentThreadOutput) implements WebSocket.Listener { @Override public void onOpen(WebSocket webSocket) { // HttpClient is calling downstream completablefutures on the CF common pool so the thread local variables are // not necessarily inherited // See // https://github.com/openjdk/jdk/blob/744e0893100d402b2b51762d57bcc2e99ab7fdcc/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java#L1069 SonarLintLogger.get().setTarget(currentThreadOutput); LOG.debug("WebSocket opened"); WebSocket.Listener.super.onOpen(webSocket); } @Override public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { SonarLintLogger.get().setTarget(currentThreadOutput); messageConsumer.accept(data.toString()); return WebSocket.Listener.super.onText(webSocket, data, last); } @Override public void onError(WebSocket webSocket, Throwable error) { SonarLintLogger.get().setTarget(currentThreadOutput); LOG.error("Error occurred on the WebSocket", error); onWebSocketInputClosedRunnable.run(); } @Override public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) { SonarLintLogger.get().setTarget(currentThreadOutput); LOG.debug("WebSocket closed, status=" + statusCode + ", reason=" + reason); onWebSocketInputClosedRunnable.run(); return null; } } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/package-info.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.http; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ssl/CertificateStore.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http.ssl; import java.nio.file.Path; public class CertificateStore { public static final String DEFAULT_PASSWORD = "sonarlint"; public static final String DEFAULT_STORE_TYPE = "PKCS12"; private final Path path; private final String keyStorePassword; private final String keyStoreType; public CertificateStore(Path path, String keyStorePassword, String keyStoreType) { this.path = path; this.keyStorePassword = keyStorePassword; this.keyStoreType = keyStoreType; } public Path getPath() { return path; } public String getKeyStorePassword() { return keyStorePassword; } public String getKeyStoreType() { return keyStoreType; } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ssl/SslConfig.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http.ssl; import javax.annotation.CheckForNull; import javax.annotation.Nullable; public class SslConfig { private final CertificateStore keyStore; private final CertificateStore trustStore; public SslConfig(@Nullable CertificateStore keyStore, @Nullable CertificateStore trustStore) { this.keyStore = keyStore; this.trustStore = trustStore; } @CheckForNull public CertificateStore getKeyStore() { return keyStore; } @CheckForNull public CertificateStore getTrustStore() { return trustStore; } } ================================================ FILE: backend/http/src/main/java/org/sonarsource/sonarlint/core/http/ssl/package-info.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.http.ssl; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/http/src/test/java/org/sonarsource/sonarlint/core/http/HttpClientProviderTests.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class HttpClientProviderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static WireMockExtension sonarqubeMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); @Test void it_should_use_user_agent() { var underTest = HttpClientProvider.forTesting(); underTest.getHttpClientWithoutAuth().get(sonarqubeMock.url("/test")); sonarqubeMock.verify(getRequestedFor(urlEqualTo("/test")) .withHeader("User-Agent", equalTo("SonarLint tests"))); } @Test void it_should_support_cancellation() { sonarqubeMock.stubFor(get("/delayed") .willReturn(aResponse() .withFixedDelay(20000))); var underTest = HttpClientProvider.forTesting(); var future = underTest.getHttpClientWithoutAuth().getAsync(sonarqubeMock.url("/delayed")); assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS)); assertThat(future.cancel(true)).isTrue(); assertThat(future).isCancelled(); assertThat(logTester.logs()).containsExactly("Request cancelled"); } @Test void it_should_preserve_post_on_permanent_moved_status() { sonarqubeMock.stubFor(post("/afterMove").willReturn(aResponse())); sonarqubeMock.stubFor(post("/permanentMoved") .willReturn(aResponse() .withStatus(HttpStatus.SC_MOVED_PERMANENTLY) .withHeader("Location", sonarqubeMock.url("/afterMove")))); HttpClientProvider.forTesting().getHttpClientWithoutAuth().post(sonarqubeMock.url("/permanentMoved"), "text/html", "Foo"); sonarqubeMock.verify(postRequestedFor(urlEqualTo("/afterMove"))); } @Test void it_should_preserve_post_on_temporarily_moved_status() { sonarqubeMock.stubFor(post("/afterMove").willReturn(aResponse())); sonarqubeMock.stubFor(post("/tempMoved") .willReturn(aResponse() .withStatus(HttpStatus.SC_MOVED_TEMPORARILY) .withHeader("Location", sonarqubeMock.url("/afterMove")))); HttpClientProvider.forTesting().getHttpClientWithoutAuth().post(sonarqubeMock.url("/tempMoved"), "text/html", "Foo"); sonarqubeMock.verify(postRequestedFor(urlEqualTo("/afterMove"))); } @Test void it_should_preserve_post_on_see_other_status() { sonarqubeMock.stubFor(post("/afterMove").willReturn(aResponse())); sonarqubeMock.stubFor(post("/seeOther") .willReturn(aResponse() .withStatus(HttpStatus.SC_SEE_OTHER) .withHeader("Location", sonarqubeMock.url("/afterMove")))); HttpClientProvider.forTesting().getHttpClientWithoutAuth().post(sonarqubeMock.url("/seeOther"), "text/html", "Foo"); sonarqubeMock.verify(postRequestedFor(urlEqualTo("/afterMove"))); } @Test void it_should_not_retry_non_idempotent_by_default() { sonarqubeMock.stubFor(post("/error").willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); var underTest = HttpClientProvider.forTesting(); underTest.getHttpClientWithoutAuth().post(sonarqubeMock.url("/error"), ContentType.TEXT_PLAIN.getMimeType(), "body"); sonarqubeMock.verify(1, postRequestedFor(urlEqualTo("/error")) .withHeader("User-Agent", equalTo("SonarLint tests"))); } } ================================================ FILE: backend/http/src/test/java/org/sonarsource/sonarlint/core/http/WebSocketClientTest.java ================================================ /* * SonarLint Core - HTTP * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.http; import java.net.URI; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class WebSocketClientTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static ExecutorService executor; @BeforeAll static void setUp() { executor = Executors.newSingleThreadExecutor(); } @AfterAll static void tearDown() { if (executor != null) { executor.shutdown(); try { if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } } @Test void should_validate_null_uri() { var client = new WebSocketClient("test-agent", "token", executor); var future = client.createWebSocketConnection(null, message -> {}, () -> {}); assertThat(future).isCompletedExceptionally(); assertThatThrownBy(future::get) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageContaining("WebSocket URI must use 'ws' or 'wss' scheme"); } @Test void should_validate_invalid_scheme() { var client = new WebSocketClient("test-agent", "token", executor); var future = client.createWebSocketConnection(URI.create("http://example.com"), message -> {}, () -> {}); assertThat(future).isCompletedExceptionally(); assertThatThrownBy(future::get) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageContaining("WebSocket URI must use 'ws' or 'wss' scheme"); } @Test void should_accept_valid_ws_uri() { var client = new WebSocketClient("test-agent", "token", executor); var future = client.createWebSocketConnection(URI.create("ws://example.com"), message -> {}, () -> {}); assertThat(future).isNotCompletedExceptionally(); } @Test void should_accept_valid_wss_uri() { var client = new WebSocketClient("test-agent", "token", executor); var future = client.createWebSocketConnection(URI.create("wss://example.com"), message -> {}, () -> {}); assertThat(future).isNotCompletedExceptionally(); } } ================================================ FILE: backend/http/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/plugin-api/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-plugin-api jar SonarLint Plugin API API used between SonarLint and analyzers com.google.code.findbugs jsr305 provided org.sonarsource.api.plugin sonar-plugin-api ${sonar-plugin-api.version} org.slf4j slf4j-api src/main/resources true sonarlint-api-version.txt org.apache.maven.plugins maven-source-plugin ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/SonarLintRuntime.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api; import org.sonar.api.Plugin; import org.sonar.api.SonarRuntime; import org.sonar.api.utils.Version; /** * Provides extra runtime related information when in the context of SonarLint. * * An instance of this class can be accessed through the context passed in {@link org.sonar.api.Plugin.Context#define(Plugin.Context)}. * @since 6.0 */ public interface SonarLintRuntime extends SonarRuntime { /** * @since 6.0 * @return the version of the sonarlint-plugin-api */ Version getSonarLintPluginApiVersion(); /** * @since 6.2 * @return the PID of the client (IDE) */ long getClientPid(); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/issue/NewInputFileEdit.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.issue; import org.sonar.api.batch.fs.InputFile; /** * Describe a file edit for a {@link NewQuickFix} as a collection of {@link NewTextEdit}s on a given {@link InputFile}. * Text edits are applied in the order they are added, insofar that their ranges do not overlap. * @since 6.3 * @deprecated use org.sonar.api.batch.sensor.issue.fix.NewInputFileEdit from the sonar-plugin-api instead */ @Deprecated(since = "8.12") public interface NewInputFileEdit { /** * @param inputFile the input file on which to apply this edit * @return the modified edit */ NewInputFileEdit on(InputFile inputFile); /** * Create a new text edit * @return a new uninitialized instance of a text edit for a given file edit */ NewTextEdit newTextEdit(); /** * Add a text edit to this input file edit * @param newTextEdit the text edit to add * @return this instance */ NewInputFileEdit addTextEdit(NewTextEdit newTextEdit); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/issue/NewQuickFix.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.issue; /** * Describe a quick fix for a {@link NewSonarLintIssue}, with a description and a collection of {@link NewInputFileEdit}. * Input file edits will be applied in the order they are added, insofar that they are compatible with one another. * @since 6.3 * @deprecated use org.sonar.api.batch.sensor.issue.fix.NewQuickFix from the sonar-plugin-api instead */ @Deprecated(since = "8.12") public interface NewQuickFix { /** * Define the message for this quick fix, which will be shown to the user as an action item. * The fix message may be inspired by the issue message, but the context into which they appear is different, * so it might be better to adapt it. A good message should: *
    *
  • Be short (ideally, not more than 50 characters)
  • *
  • Use sentence capitalization
  • *
  • Not end with a full stop (.)
  • *
  • Describe the expected outcome of the change, e.g. Make the constructor explicit instead of Add the "explicit" keyword. * It tells the user how to fix the issue
  • *
  • Focus on the target more than the current situation. For instance, Replace "AAA" with "BBB" would be better phrased Replace with "BBB"
  • *
  • Avoid the use of a demonstrative, e.g. this. Prefer the more neutral the. * The message may be used in several contexts, some of which would not work very well with a demonstrative
  • *
* @param message a description for this quick fix * @return the updated quickfix */ NewQuickFix message(String message); /** * Create a new input file edit * @return a new uninitialized instance of a file edit for a given fix */ NewInputFileEdit newInputFileEdit(); /** * Add a new input file edit to this quick fix * @param newInputFileEdit the input file edit to add * @return this instance */ NewQuickFix addInputFileEdit(NewInputFileEdit newInputFileEdit); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/issue/NewSonarLintIssue.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.issue; /** * Extension interface to add {@link NewQuickFix}es to a {@link org.sonar.api.batch.sensor.issue.NewIssue} * @since 6.3 * @deprecated use org.sonar.api.batch.sensor.issue.NewIssue from the sonar-plugin-api instead */ @Deprecated(since = "8.12") public interface NewSonarLintIssue { /** * Create a new quick fix * @return a new uninitialized instance of a quick fix for a given issue */ NewQuickFix newQuickFix(); /** * Add a new quick fix to this issue * @param newQuickFix the quick fix to add * @return this object */ NewSonarLintIssue addQuickFix(NewQuickFix newQuickFix); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/issue/NewTextEdit.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.issue; import org.sonar.api.batch.fs.TextRange; /** * Describe a text edit for a {@link NewInputFileEdit} as a replacement text for a given {@link TextRange} * @since 6.3 * @deprecated use org.sonar.api.batch.sensor.issue.fix.NewTextEdit from the sonar-plugin-api instead */ @Deprecated(since = "8.12") public interface NewTextEdit { /** * @param range the range on which to apply this edit * @return the modified edit */ NewTextEdit at(TextRange range); /** * Prior to 6.4, line returns had to be represented with the '\n' character. * From 6.4 on, analyzers can use any EOL character they see fit, SonarLint takes care of adapting this to the one * expected by the IDE. * To remove code, use the empty string (""). * When removing some code from the source file, make sure that no lines consisting only of whitespaces remain. * If after the code is removed a non-whitespace character remains, place it at the same indentation level as the removed code. * @param newText the replacement text. * @return the modified edit */ NewTextEdit withNewText(String newText); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/issue/package-info.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.plugin.api.issue; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/module/file/ModuleFileEvent.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.module.file; import org.sonar.api.batch.fs.InputFile; /** * @since 6.0 */ public interface ModuleFileEvent { /** * @return the InputFile concerned by the event * @since 6.0 */ InputFile getTarget(); /** * @return the event Type * @since 6.0 */ Type getType(); /** * @since 6.0 */ enum Type { CREATED, MODIFIED, DELETED } } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/module/file/ModuleFileListener.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.module.file; /** * Implement this interface and annotate the class with {@code @SonarLintSide(MODULE)} to receive events related to the module file system. * @since 6.0 */ public interface ModuleFileListener { /** * React to a file creation, deletion or modification event * * @param event an event that concerns a file in the module file system * @since 6.0 */ void process(ModuleFileEvent event); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/module/file/ModuleFileSystem.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.plugin.api.module.file; import java.util.stream.Stream; import org.sonar.api.batch.fs.InputFile; /** * This class is made available to components annotated with {@code @SonarLintSide(MODULE)}. * @since 6.0 */ public interface ModuleFileSystem { /** * Returns all the files within the module that end with {@code suffix} and match {@code type}. * * @param suffix a suffix to filter the files * @param type the type of file * @return a stream of files that match the given suffix and type in the module * @since 6.0 */ Stream files(String suffix, InputFile.Type type); /** * Returns all the files within the module. * * @return a stream of module files * @since 6.0 */ Stream files(); } ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/module/file/package-info.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.plugin.api.module.file; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-api/src/main/java/org/sonarsource/sonarlint/plugin/api/package-info.java ================================================ /* * SonarLint Plugin API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.plugin.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-api/src/main/resources/sonarlint-api-version.txt ================================================ ${project.version} ================================================ FILE: backend/plugin-commons/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-plugin-commons SonarLint Core - Plugin Commons Common code used to load/execute SonarQube plugins com.google.code.findbugs jsr305 provided ${project.groupId} sonarlint-commons ${project.version} ${project.groupId} sonarlint-plugin-api ${project.version} org.sonarsource.api.plugin sonar-plugin-api ${sonar-plugin-api.version} org.slf4j slf4j-api org.springframework spring-context org.apache.commons commons-lang3 commons-io commons-io org.sonarsource.classloader sonar-classloader org.apache.commons commons-csv 1.14.1 org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test org.assertj assertj-core test org.mockito mockito-core test commons-codec commons-codec test jakarta.annotation jakarta.annotation-api test javax.annotation javax.annotation-api test ch.qos.logback logback-classic test conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/plugin-commons/src/main/java/com/sonarsource/plugins/license/api/LicensedPluginRegistration.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.sonarsource.plugins.license.api; public class LicensedPluginRegistration { private final String pluginKey; private LicensedPluginRegistration(Builder builder) { this.pluginKey = builder.pluginKey; } public String getPluginKey() { return pluginKey; } public static LicensedPluginRegistration forPlugin(String pluginKey) { return new Builder().setPluginKey(pluginKey).build(); } public static final class Builder { private String pluginKey; public Builder setPluginKey(String s) { this.pluginKey = s; return this; } public LicensedPluginRegistration build() { return new LicensedPluginRegistration(this); } } } ================================================ FILE: backend/plugin-commons/src/main/java/com/sonarsource/plugins/license/api/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package com.sonarsource.plugins.license.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonar/api/SonarQubeVersion.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.api; import javax.annotation.concurrent.Immutable; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.scanner.ScannerSide; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.Version; import static java.util.Objects.requireNonNull; /** * This class was removed from the plugin API but still used in older CFamily analyzer */ @ScannerSide @ServerSide @ComputeEngineSide @Immutable @Deprecated public class SonarQubeVersion { private final Version version; public SonarQubeVersion(Version version) { requireNonNull(version); this.version = version; } public Version get() { return this.version; } public boolean isGreaterThanOrEqual(Version than) { return this.version.isGreaterThanOrEqual(than); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonar/api/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonar.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/ApiVersions.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Scanner; import org.sonar.api.utils.Version; public class ApiVersions { static final String SONAR_PLUGIN_API_VERSION_FILE_PATH = "/sonar-api-version.txt"; private static final String SONARLINT_PLUGIN_API_VERSION_FILE_PATH = "/sonarlint-api-version.txt"; private ApiVersions() { // only static methods } public static Version loadSonarPluginApiVersion() { return loadVersion(SONAR_PLUGIN_API_VERSION_FILE_PATH); } public static Version loadSonarLintPluginApiVersion() { return loadVersion(SONARLINT_PLUGIN_API_VERSION_FILE_PATH); } private static Version loadVersion(String versionFilePath) { return loadVersion(ApiVersions.class.getResource(versionFilePath), versionFilePath); } static Version loadVersion(URL versionFileURL, String versionFilePath) { try (var scanner = new Scanner(versionFileURL.openStream(), StandardCharsets.UTF_8)) { var versionInFile = scanner.nextLine(); return Version.parse(versionInFile); } catch (Exception e) { throw new IllegalStateException("Can not load " + versionFilePath + " from classpath", e); } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/DataflowBugDetection.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.util.Set; public class DataflowBugDetection { private DataflowBugDetection() { // Static stuff only } public static final Set PLUGIN_ALLOW_LIST = Set.of("dbd", "dbdpythonfrontend", "dbdjavafrontend"); static Set getPluginAllowList(boolean isDataflowBugDetectionEnabled) { return isDataflowBugDetectionEnabled ? PLUGIN_ALLOW_LIST : Set.of(); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/ExtensionInstaller.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.util.Map; import java.util.Map.Entry; import java.util.function.BiPredicate; import org.sonar.api.Plugin; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.container.ExtensionContainer; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.PluginContextImpl; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; public class ExtensionInstaller { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarLintRuntime sonarRuntime; private final Configuration bootConfiguration; public ExtensionInstaller(SonarLintRuntime sonarRuntime, Configuration bootConfiguration) { this.sonarRuntime = sonarRuntime; this.bootConfiguration = bootConfiguration; } public void install(ExtensionContainer container, Map pluginInstancesByKey, BiPredicate extensionFilter) { for (Entry pluginInstanceEntry : pluginInstancesByKey.entrySet()) { var plugin = pluginInstanceEntry.getValue(); var context = new PluginContextImpl.Builder() .setSonarRuntime(sonarRuntime) .setBootConfiguration(bootConfiguration) .build(); var pluginKey = pluginInstanceEntry.getKey(); try { plugin.define(context); loadExtensions(container, pluginKey, context, extensionFilter); } catch (Throwable t) { LOG.error("Error loading components for plugin '{}'", pluginKey, t); } } } private static void loadExtensions(ExtensionContainer container, String pluginKey, Plugin.Context context, BiPredicate extensionFilter) { for (Object extension : context.getExtensions()) { if (extensionFilter.test(pluginKey, extension)) { container.addExtension(pluginKey, extension); } } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/ExtensionUtils.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.ScannerSide; import org.sonar.api.utils.AnnotationUtils; import org.sonarsource.api.sonarlint.SonarLintSide; public class ExtensionUtils { private ExtensionUtils() { // only static methods } public static boolean isInstantiationStrategy(Object extension, String strategy) { var annotation = AnnotationUtils.getAnnotation(extension, InstantiationStrategy.class); if (annotation != null) { return strategy.equals(annotation.value()); } return InstantiationStrategy.PER_PROJECT.equals(strategy); } public static boolean isSonarLintSide(Object extension) { return AnnotationUtils.getAnnotation(extension, SonarLintSide.class) != null; } public static boolean isScannerSide(Object extension) { return AnnotationUtils.getAnnotation(extension, ScannerSide.class) != null || AnnotationUtils.getAnnotation(extension, SonarLintSide.class) != null; } public static boolean isType(Object extension, Class extensionClass) { var clazz = extension instanceof Class ? (Class) extension : extension.getClass(); return extensionClass.isAssignableFrom(clazz); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/LoadedPlugins.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.io.IOException; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.sonar.api.Plugin; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInstancesLoader; public class LoadedPlugins { private final Map pluginInstancesByKeys; private final PluginInstancesLoader pluginInstancesLoader; private final Set additionalAllowedPlugins; private final Set disabledPluginKeys; public LoadedPlugins(Map pluginInstancesByKeys, PluginInstancesLoader pluginInstancesLoader, Set additionalAllowedPlugins, Set disabledPluginKeys) { this.pluginInstancesByKeys = pluginInstancesByKeys; this.pluginInstancesLoader = pluginInstancesLoader; this.additionalAllowedPlugins = additionalAllowedPlugins; this.disabledPluginKeys = disabledPluginKeys; } public Map getAllPluginInstancesByKeys() { return pluginInstancesByKeys; } public Map getAnalysisPluginInstancesByKeys() { return pluginInstancesByKeys.entrySet().stream() .filter(entry -> !disabledPluginKeys.contains(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } public Set getAdditionalAllowedPlugins() { return additionalAllowedPlugins; } public void close() throws IOException { // close plugins classloaders pluginInstancesByKeys.clear(); pluginInstancesLoader.close(); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/MultivalueProperty.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.io.IOException; import java.io.StringReader; import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import org.apache.commons.lang3.ArrayUtils; import static java.util.function.UnaryOperator.identity; public class MultivalueProperty { private MultivalueProperty() { // prevents instantiation } public static String[] parseAsCsv(String key, String value) { return parseAsCsv(key, value, identity()); } public static String[] parseAsCsv(String key, String value, UnaryOperator valueProcessor) { String cleanValue = MultivalueProperty.trimFieldsAndRemoveEmptyFields(value); List result = new ArrayList<>(); try (var csvParser = CSVFormat.RFC4180.builder() .setSkipHeaderRecord(true) .setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true) .get() .parse(new StringReader(cleanValue))) { List records = csvParser.getRecords(); if (records.isEmpty()) { return ArrayUtils.EMPTY_STRING_ARRAY; } processRecords(result, records, valueProcessor); return result.toArray(new String[0]); } catch (IOException | UncheckedIOException e) { throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e); } } /** * In most cases we expect a single record.
Having multiple records means the input value was splitted over multiple lines (this is common in Maven). * For example: *
   *   <sonar.exclusions>
   *     src/foo,
   *     src/bar,
   *     src/biz
   *   <sonar.exclusions>
   * 
* In this case records will be merged to form a single list of items. Last item of a record is appended to first item of next record. *

* This is a very curious case, but we try to preserve line break in the middle of an item: *

   *   <sonar.exclusions>
   *     a
   *     b,
   *     c
   *   <sonar.exclusions>
   * 
* will produce ['a\nb', 'c'] */ private static void processRecords(List result, List records, UnaryOperator valueProcessor) { for (CSVRecord csvRecord : records) { Iterator it = csvRecord.iterator(); if (!result.isEmpty()) { String next = it.next(); if (!next.isEmpty()) { int lastItemIdx = result.size() - 1; String previous = result.get(lastItemIdx); if (previous.isEmpty()) { result.set(lastItemIdx, valueProcessor.apply(next)); } else { result.set(lastItemIdx, valueProcessor.apply(previous + "\n" + next)); } } } it.forEachRemaining(s -> { String apply = valueProcessor.apply(s); result.add(apply); }); } } /** * Removes the empty fields from the value of a multi-value property from empty fields, including trimming each field. *

* Quotes can be used to prevent an empty field to be removed (as it is used to preserve empty spaces). *

    *
  • {@code "" => ""}
  • *
  • {@code " " => ""}
  • *
  • {@code "," => ""}
  • *
  • {@code ",," => ""}
  • *
  • {@code ",,," => ""}
  • *
  • {@code ",a" => "a"}
  • *
  • {@code "a," => "a"}
  • *
  • {@code ",a," => "a"}
  • *
  • {@code "a,,b" => "a,b"}
  • *
  • {@code "a, ,b" => "a,b"}
  • *
  • {@code "a,\"\",b" => "a,b"}
  • *
  • {@code "\"a\",\"b\"" => "\"a\",\"b\""}
  • *
  • {@code "\" a \",\"b \"" => "\" a \",\"b \""}
  • *
  • {@code "\"a\",\"\",\"b\"" => "\"a\",\"\",\"b\""}
  • *
  • {@code "\"a\",\" \",\"b\"" => "\"a\",\" \",\"b\""}
  • *
  • {@code "\" a,,b,c \",\"d \"" => "\" a,,b,c \",\"d \""}
  • *
  • {@code "a,\" \",b" => "ab"]}
  • *
*/ static String trimFieldsAndRemoveEmptyFields(String str) { char[] chars = str.toCharArray(); var res = new char[chars.length]; /* * set when reading the first non trimmable char after a separator char (or the beginning of the string) * unset when reading a separator */ var inField = false; var inQuotes = false; var i = 0; var resI = 0; for (; i < chars.length; i++) { boolean isSeparator = chars[i] == ','; if (!inQuotes && isSeparator) { // exiting field (may already be unset) inField = false; if (resI > 0) { resI = retroTrim(res, resI); } } else { boolean isTrimmed = !inQuotes && istrimmable(chars[i]); if (isTrimmed && !inField) { // we haven't met any non trimmable char since the last separator yet continue; } boolean isEscape = isEscapeChar(chars[i]); if (isEscape) { inQuotes = !inQuotes; } // add separator as we already had one field if (!inField && resI > 0) { res[resI] = ','; resI++; } // register in field (may already be set) inField = true; // copy current char res[resI] = chars[i]; resI++; } } // inQuotes can only be true at this point if quotes are unbalanced if (!inQuotes) { // trim end of str resI = retroTrim(res, resI); } return new String(res, 0, resI); } private static boolean isEscapeChar(char aChar) { return aChar == '"'; } private static boolean istrimmable(char aChar) { return aChar <= ' '; } /** * Reads from index {@code resI} to the beginning into {@code res} looking up the location of the trimmable char with * the lowest index before encountering a non-trimmable char. *

* This basically trims {@code res} from any trimmable char at its end. * * @return index of next location to put new char in res */ private static int retroTrim(char[] res, int resI) { int i = resI; while (i >= 1) { if (!istrimmable(res[i - 1])) { return i; } i--; } return i; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/PluginsLoadResult.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.util.Map; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginRequirementsCheckResult; public class PluginsLoadResult { private final LoadedPlugins loadedPlugins; private final Map pluginCheckResultByKeys; PluginsLoadResult(LoadedPlugins loadedPlugins, Map pluginCheckResultByKeys) { this.loadedPlugins = loadedPlugins; this.pluginCheckResultByKeys = pluginCheckResultByKeys; } public LoadedPlugins getLoadedPlugins() { return loadedPlugins; } public Map getPluginCheckResultByKeys() { return pluginCheckResultByKeys; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/PluginsLoader.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import org.sonar.api.utils.System2; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInfo; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInstancesLoader; import org.sonarsource.sonarlint.core.plugin.commons.loading.PluginRequirementsCheckResult; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginRequirementsChecker; import static java.util.function.Predicate.not; /** * Orchestrates the loading and instantiation of plugins */ public class PluginsLoader { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final SonarPluginRequirementsChecker requirementsChecker = new SonarPluginRequirementsChecker(); public static class Configuration { private final Set pluginJarLocations; private final Set enabledLanguages; private final Optional nodeCurrentVersion; private final boolean enableDataflowBugDetection; public Configuration(Set pluginJarLocations, Set enabledLanguages, boolean enableDataflowBugDetection, Optional nodeCurrentVersion) { this.pluginJarLocations = pluginJarLocations; this.enabledLanguages = enabledLanguages; this.nodeCurrentVersion = nodeCurrentVersion; this.enableDataflowBugDetection = enableDataflowBugDetection; } } public PluginsLoadResult load(Configuration configuration, Set disabledPluginsForAnalysis) { var javaSpecVersion = Objects.requireNonNull(System2.INSTANCE.property("java.specification.version"), "Missing Java property 'java.specification.version'"); var pluginCheckResultByKeys = requirementsChecker.checkRequirements(configuration.pluginJarLocations, configuration.enabledLanguages, Version.create(javaSpecVersion), configuration.nodeCurrentVersion, configuration.enableDataflowBugDetection); var nonSkippedPlugins = getNonSkippedPlugins(pluginCheckResultByKeys); logPlugins(nonSkippedPlugins); var instancesLoader = new PluginInstancesLoader(); var pluginInstancesByKeys = instancesLoader.instantiatePluginClasses(nonSkippedPlugins); return new PluginsLoadResult(new LoadedPlugins(pluginInstancesByKeys, instancesLoader, additionalAllowedPlugins(configuration), disabledPluginsForAnalysis), pluginCheckResultByKeys); } private static Set additionalAllowedPlugins(Configuration configuration) { var allowedPluginsIds = new HashSet(); allowedPluginsIds.add("textdeveloper"); allowedPluginsIds.add("textenterprise"); allowedPluginsIds.add("omnisharp"); allowedPluginsIds.add("sqvsroslyn"); allowedPluginsIds.add("iacenterprise"); allowedPluginsIds.add("goenterprise"); allowedPluginsIds.addAll(maybeDbdAllowedPlugins(configuration.enableDataflowBugDetection)); return Collections.unmodifiableSet(allowedPluginsIds); } private static Set maybeDbdAllowedPlugins(boolean enableDataflowBugDetection) { return DataflowBugDetection.getPluginAllowList(enableDataflowBugDetection); } private static void logPlugins(Collection nonSkippedPlugins) { LOG.debug("Loaded {} plugins", nonSkippedPlugins.size()); for (PluginInfo p : nonSkippedPlugins) { LOG.debug(" * {} {} ({})", p.getName(), p.getVersion(), p.getKey()); } } private static Collection getNonSkippedPlugins(Map pluginCheckResultByKeys) { return pluginCheckResultByKeys.values().stream() .filter(not(PluginRequirementsCheckResult::isSkipped)) .map(PluginRequirementsCheckResult::getPlugin) .toList(); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/api/SkipReason.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.api; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public interface SkipReason { class UnsupportedFeature implements SkipReason { public static final UnsupportedFeature INSTANCE = new UnsupportedFeature(); private UnsupportedFeature() { // Singleton } } class IncompatiblePluginApi implements SkipReason { public static final IncompatiblePluginApi INSTANCE = new IncompatiblePluginApi(); private IncompatiblePluginApi() { // Singleton } } class LanguagesNotEnabled implements SkipReason { private final Set languages; public LanguagesNotEnabled(Collection languages) { this.languages = new LinkedHashSet<>(languages); } public Set getNotEnabledLanguages() { return languages; } @Override public int hashCode() { return Objects.hash(languages); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof LanguagesNotEnabled other)) { return false; } return Objects.equals(languages, other.languages); } @Override public String toString() { return "LanguagesNotEnabled [languages=" + languages + "]"; } } class UnsatisfiedDependency implements SkipReason { private final String dependencyKey; public UnsatisfiedDependency(String dependencyKey) { this.dependencyKey = dependencyKey; } public String getDependencyKey() { return dependencyKey; } @Override public int hashCode() { return Objects.hash(dependencyKey); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof UnsatisfiedDependency other)) { return false; } return Objects.equals(dependencyKey, other.dependencyKey); } @Override public String toString() { return "UnsatisfiedDependency [dependencyKey=" + dependencyKey + "]"; } } class UnsatisfiedRuntimeRequirement implements SkipReason { public enum RuntimeRequirement { JRE, NODEJS } private final RuntimeRequirement runtime; private final String currentVersion; private final String minVersion; public UnsatisfiedRuntimeRequirement(RuntimeRequirement runtime, @Nullable String currentVersion, String minVersion) { this.runtime = runtime; this.currentVersion = currentVersion; this.minVersion = minVersion; } public RuntimeRequirement getRuntime() { return runtime; } @CheckForNull public String getCurrentVersion() { return currentVersion; } public String getMinVersion() { return minVersion; } @Override public int hashCode() { return Objects.hash(runtime, currentVersion, minVersion); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof UnsatisfiedRuntimeRequirement other)) { return false; } return runtime == other.runtime && Objects.equals(currentVersion, other.currentVersion) && Objects.equals(minVersion, other.minVersion); } @Override public String toString() { return "UnsatisfiedRuntimeRequirement [runtime=" + runtime + ", currentVersion=" + currentVersion + ", minVersion=" + minVersion + "]"; } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/api/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.commons.api; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/ClassDerivedBeanDefinition.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.lang.reflect.Constructor; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Nullable; /** * Taken from Spring's GenericApplicationContext.ClassDerivedBeanDefinition. * The goal is to support multiple constructors when adding extensions for plugins when no annotations are present. * Spring will pick the constructor with the highest number of arguments that it can inject. */ public class ClassDerivedBeanDefinition extends RootBeanDefinition { public ClassDerivedBeanDefinition(Class beanClass) { super(beanClass); } public ClassDerivedBeanDefinition(ClassDerivedBeanDefinition original) { super(original); } /** * This method gets called from AbstractAutowireCapableBeanFactory#createBeanInstance when a bean is instantiated. * It first tries to look at annotations or any other methods provided by bean post processors. If a constructor can't be determined, it will fallback to this method. */ @Override @Nullable public Constructor[] getPreferredConstructors() { Class clazz = getBeanClass(); Constructor primaryCtor = BeanUtils.findPrimaryConstructor(clazz); if (primaryCtor != null) { return new Constructor[] {primaryCtor}; } Constructor[] publicCtors = clazz.getConstructors(); if (publicCtors.length > 0) { return publicCtors; } return null; } @Override public RootBeanDefinition cloneBeanDefinition() { return new ClassDerivedBeanDefinition(this); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/ComponentKeys.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; class ComponentKeys { private static final Pattern IDENTITY_HASH_PATTERN = Pattern.compile(".+@[a-f0-9]+"); private final Set> objectsWithoutToString = new HashSet<>(); Object of(Object component) { return of(component, SonarLintLogger.get()); } Object of(Object component, SonarLintLogger log) { if (component instanceof Class) { return component; } return ofInstance(component, log); } public String ofInstance(Object component) { return ofInstance(component, SonarLintLogger.get()); } public String ofClass(Class clazz) { return clazz.getClassLoader() + "-" + clazz.getCanonicalName(); } String ofInstance(Object component, SonarLintLogger log) { var key = component.toString(); if (IDENTITY_HASH_PATTERN.matcher(key).matches()) { if (!objectsWithoutToString.add(component.getClass())) { log.warn(String.format("Bad component key: %s. Please implement toString() method on class %s", key, component.getClass().getName())); } key += UUID.randomUUID().toString(); } return ofClass(component.getClass()) + "-" + key; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/Container.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.util.List; import java.util.Optional; public interface Container { Container add(Object... objects); T getComponentByType(Class type); Optional getOptionalComponentByType(Class type); List getComponentsByType(Class type); Container getParent(); } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/ExtensionContainer.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import javax.annotation.CheckForNull; import javax.annotation.Nullable; public interface ExtensionContainer extends Container { ExtensionContainer addExtension(@Nullable String pluginKey, Object extension); ExtensionContainer declareProperties(Object extension); @Override @CheckForNull ExtensionContainer getParent(); } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/LazyUnlessStartableStrategy.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import javax.annotation.Nullable; import org.sonar.api.Startable; import org.springframework.beans.factory.config.BeanDefinition; public class LazyUnlessStartableStrategy extends SpringInitStrategy { @Override protected boolean isLazyInit(BeanDefinition beanDefinition, @Nullable Class clazz) { return clazz == null || !Startable.class.isAssignableFrom(clazz); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/PriorityBeanFactory.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.springframework.beans.BeanWrapper; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; public class PriorityBeanFactory extends DefaultListableBeanFactory { /** * Determines highest priority of the bean candidates. * Does not take into account the @Primary annotations. * This gets called from {@link DefaultListableBeanFactory#determineAutowireCandidate} when the bean factory is finding the beans to autowire. That method * checks for @Primary before calling this method. * * The strategy is to look at the @Priority annotations. If there are ties, we give priority to components that were added to child containers over their parents. * If there are still ties, null is returned, which will ultimately cause Spring to throw a NoUniqueBeanDefinitionException. */ @Override @Nullable protected String determineHighestPriorityCandidate(Map candidates, Class requiredType) { List candidateBeans = candidates.entrySet().stream() .filter(e -> e.getValue() != null) .map(e -> new Bean(e.getKey(), e.getValue())) .toList(); List beansAfterPriority = highestPriority(candidateBeans, b -> getPriority(b.getInstance())); if (beansAfterPriority.isEmpty()) { return null; } else if (beansAfterPriority.size() == 1) { return beansAfterPriority.get(0).getName(); } List beansAfterHierarchy = highestPriority(beansAfterPriority, b -> getHierarchyPriority(b.getName())); if (beansAfterHierarchy.size() == 1) { return beansAfterHierarchy.get(0).getName(); } return null; } private static List highestPriority(List candidates, Function priorityFunction) { List highestPriorityBeans = new ArrayList<>(); Integer highestPriority = null; for (Bean candidate : candidates) { Integer candidatePriority = priorityFunction.apply(candidate); if (candidatePriority == null) { candidatePriority = Integer.MAX_VALUE; } if (highestPriority == null) { highestPriority = candidatePriority; highestPriorityBeans.add(candidate); } else if (candidatePriority < highestPriority) { highestPriorityBeans.clear(); highestPriority = candidatePriority; highestPriorityBeans.add(candidate); } else if (candidatePriority.equals(highestPriority)) { highestPriorityBeans.add(candidate); } } return highestPriorityBeans; } @CheckForNull private Integer getHierarchyPriority(String beanName) { DefaultListableBeanFactory factory = this; var i = 1; while (factory != null) { if (factory.containsBeanDefinition(beanName)) { return i; } factory = (DefaultListableBeanFactory) factory.getParentBeanFactory(); i++; } return null; } /** * A common mistake when migrating from Pico Container to Spring is to forget to add @Inject or @Autowire annotations to classes that have multiple constructors. * Spring will fail if there is no default no-arg constructor, but it will silently use the no-arg constructor if there is one, never calling the other constructors. * We override this method to fail fast if a class has multiple constructors. */ @Override protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) { if (mbd.hasBeanClass() && mbd.getBeanClass().getConstructors().length > 1) { throw new IllegalStateException("Constructor annotations missing in: " + mbd.getBeanClass()); } return super.instantiateBean(beanName, mbd); } private static class Bean { private final String name; private final Object instance; public Bean(String name, Object instance) { this.name = name; this.instance = instance; } public String getName() { return name; } public Object getInstance() { return instance; } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/SpringComponentContainer.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Supplier; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.utils.System2; import org.sonarsource.sonarlint.core.commons.tracing.Step; import org.sonarsource.sonarlint.core.commons.tracing.Trace; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static java.util.Collections.emptyList; import static org.sonarsource.sonarlint.core.commons.tracing.Trace.startChildren; public class SpringComponentContainer implements StartableContainer { protected final AnnotationConfigApplicationContext context; @Nullable protected final SpringComponentContainer parent; protected final List children = new ArrayList<>(); private final PropertyDefinitions propertyDefinitions; private final ComponentKeys componentKeys = new ComponentKeys(); @Nullable private Trace trace; public SpringComponentContainer() { this(null, new PropertyDefinitions(System2.INSTANCE), emptyList(), new LazyUnlessStartableStrategy()); } protected SpringComponentContainer(List externalExtensions) { this(null, new PropertyDefinitions(System2.INSTANCE), externalExtensions, new LazyUnlessStartableStrategy()); } protected SpringComponentContainer(SpringComponentContainer parent) { this(parent, parent.propertyDefinitions, emptyList(), new LazyUnlessStartableStrategy()); } protected SpringComponentContainer(SpringComponentContainer parent, SpringInitStrategy initStrategy) { this(parent, parent.propertyDefinitions, emptyList(), initStrategy); } protected SpringComponentContainer(@Nullable SpringComponentContainer parent, PropertyDefinitions propertyDefs, List externalExtensions, SpringInitStrategy initStrategy) { this.parent = parent; this.propertyDefinitions = propertyDefs; this.context = new AnnotationConfigApplicationContext(new PriorityBeanFactory()); this.context.setAllowBeanDefinitionOverriding(false); ((AbstractAutowireCapableBeanFactory) context.getBeanFactory()).setParameterNameDiscoverer(null); if (parent != null) { context.setParent(parent.context); parent.children.add(this); } add(initStrategy); add(this); add(new StartableBeanPostProcessor()); add(externalExtensions); add(propertyDefs); } /** * Beans need to have a unique name, otherwise they'll override each other. * The strategy is: * - For classes, use the classloader + fully qualified class name as the name of the bean * - For instances, use the Classloader + FQCN + toString() * - If the object is a collection, iterate through the elements and apply the same strategy for each of them */ @Override public Container add(Object... objects) { for (Object o : objects) { if (o instanceof Class clazz) { context.registerBean(componentKeys.ofClass(clazz), clazz); declareProperties(o); } else if (o instanceof Iterable) { ((Iterable) o).forEach(this::add); } else { registerInstance(o); declareProperties(o); } } return this; } private void registerInstance(T instance) { Supplier supplier = () -> instance; Class clazz = (Class) instance.getClass(); context.registerBean(componentKeys.ofInstance(instance), clazz, supplier); } /** * Extensions are usually added by plugins and we assume they don't support any injection-related annotations. * Spring contexts supporting annotations will fail if multiple constructors are present without any annotations indicating which one to use for injection. * For that reason, we need to create the beans ourselves, using ClassDerivedBeanDefinition, which will declare that all constructors can be used for injection. */ private void addExtension(Object o) { if (o instanceof Class clazz) { var bd = new ClassDerivedBeanDefinition(clazz); context.registerBeanDefinition(componentKeys.ofClass(clazz), bd); } else if (o instanceof Iterable) { ((Iterable) o).forEach(this::addExtension); } else { registerInstance(o); } } @Override public T getComponentByType(Class type) { try { return context.getBean(type); } catch (Exception t) { throw new IllegalStateException("Unable to load component " + type, t); } } @Override public Optional getOptionalComponentByType(Class type) { try { return Optional.of(context.getBean(type)); } catch (NoSuchBeanDefinitionException t) { return Optional.empty(); } } @Override public List getComponentsByType(Class type) { try { return new ArrayList<>(context.getBeansOfType(type).values()); } catch (Exception t) { throw new IllegalStateException("Unable to load components " + type, t); } } public AnnotationConfigApplicationContext context() { return context; } public void execute(@Nullable Trace trace) { this.trace = trace; RuntimeException r = null; try { startComponents(); } catch (RuntimeException e) { r = e; } finally { try { stopComponents(); } catch (RuntimeException e) { if (r == null) { r = e; } } } if (r != null) { throw r; } } @Override public SpringComponentContainer startComponents() { startChildren(trace, "startComponents", new Step("doBeforeStart", this::doBeforeStart), new Step("refresh", context::refresh), new Step("doAfterStart", this::doAfterStart) ); return this; } public SpringComponentContainer stopComponents() { try { stopChildren(); if (context.isActive()) { context.close(); } } finally { if (parent != null) { parent.children.remove(this); } } return this; } private void stopChildren() { // loop over a copy of list of children in reverse order var childrenCopy = new ArrayList<>(this.children); childrenCopy.reversed().forEach(SpringComponentContainer::stopComponents); } public SpringComponentContainer createChild() { return new SpringComponentContainer(this); } @Override @CheckForNull public SpringComponentContainer getParent() { return parent; } @Override public SpringComponentContainer addExtension(@Nullable String pluginKey, Object extension) { try { addExtension(extension); } catch (Throwable t) { throw new IllegalStateException("Unable to register extension " + getName(extension) + (pluginKey != null ? (" from plugin '" + pluginKey + "'") : ""), t); } declareProperties(extension); return this; } private static String getName(Object extension) { if (extension instanceof Class) { return ((Class) extension).getName(); } return getName(extension.getClass()); } @Override public SpringComponentContainer declareProperties(Object extension) { this.propertyDefinitions.addComponent(extension, ""); return this; } /** * This method aims to be overridden */ protected void doBeforeStart() { // nothing } /** * This method aims to be overridden */ protected void doAfterStart() { // nothing } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/SpringInitStrategy.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import javax.annotation.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; public abstract class SpringInitStrategy implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { for (String beanName : beanFactory.getBeanDefinitionNames()) { var beanDefinition = beanFactory.getBeanDefinition(beanName); Class rawClass = beanDefinition.getResolvableType().getRawClass(); beanDefinition.setLazyInit(isLazyInit(beanDefinition, rawClass)); } } protected abstract boolean isLazyInit(BeanDefinition beanDefinition, @Nullable Class clazz); } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/StartableBeanPostProcessor.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import org.sonar.api.Startable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.lang.Nullable; public class StartableBeanPostProcessor implements DestructionAwareBeanPostProcessor { @Override @Nullable public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Startable startable) { startable.start(); } return bean; } @Override public boolean requiresDestruction(Object bean) { return bean instanceof Startable; } @Override public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { try { // note: Spring will call close() on AutoCloseable beans. if (bean instanceof Startable startable) { startable.stop(); } } catch (Exception e) { SonarLintLogger.get() .warn("Dispose of component {} failed", bean.getClass().getCanonicalName(), e); } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/StartableContainer.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; public interface StartableContainer extends ExtensionContainer { StartableContainer startComponents(); } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/container/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.commons.container; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginClassLoaderDef.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.sonar.classloader.Mask; import static org.apache.commons.lang3.StringUtils.isNotEmpty; /** * Temporary information about the classLoader to be created for a plugin (or a group of plugins). */ class PluginClassLoaderDef { private final String basePluginKey; private final Map mainClassesByPluginKey = new HashMap<>(); private final List files = new ArrayList<>(); private final Mask.Builder mask = Mask.builder(); PluginClassLoaderDef(String basePluginKey) { this.basePluginKey = basePluginKey; } String getBasePluginKey() { return basePluginKey; } List getFiles() { return files; } void addFiles(Collection f) { this.files.addAll(f); } Mask.Builder getExportMaskBuilder() { return mask; } Map getMainClassesByPluginKey() { return mainClassesByPluginKey; } void addMainClass(String pluginKey, @Nullable String mainClass) { if (isNotEmpty(mainClass)) { mainClassesByPluginKey.put(pluginKey, mainClass); } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginClassloaderFactory.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Map; import org.sonar.classloader.ClassloaderBuilder; import org.sonar.classloader.Mask; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.PARENT_FIRST; /** * Builds the graph of classloaders to be used to instantiate plugins. It deals with: *

    *
  • isolation of plugins against core classes (except api)
  • *
  • backward-compatibility with plugins built for versions of SQ lower than 5.2. At that time * API declared transitive dependencies that were automatically available to plugins
  • *
  • sharing of some packages between plugins
  • *
  • loading of the libraries embedded in plugin JAR files (directory META-INF/libs)
  • *
*/ @SonarLintSide public class PluginClassloaderFactory { private static final SonarLintLogger LOG = SonarLintLogger.get(); // underscores are used to not conflict with plugin keys (if someday a plugin key is "api") private static final String API_CLASSLOADER_KEY = "_api_"; /** * Creates as many classloaders as requested by the input parameter. */ Map create(ClassLoader baseClassLoader, Collection defs) { var builder = new ClassloaderBuilder(); builder.newClassloader(API_CLASSLOADER_KEY, baseClassLoader); builder.setMask(API_CLASSLOADER_KEY, apiMask()); for (var def : defs) { builder.newClassloader(def.getBasePluginKey()); builder.setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, Mask.ALL); builder.setLoadingOrder(def.getBasePluginKey(), PARENT_FIRST); for (var jar : def.getFiles()) { builder.addURL(def.getBasePluginKey(), fileToUrl(jar)); } exportResources(def, builder, defs); } return build(defs, builder); } /** * A plugin can export some resources to other plugins */ private static void exportResources(PluginClassLoaderDef def, ClassloaderBuilder builder, Collection allPlugins) { // export the resources to all other plugins builder.setExportMask(def.getBasePluginKey(), def.getExportMaskBuilder().build()); for (var other : allPlugins) { if (!other.getBasePluginKey().equals(def.getBasePluginKey())) { builder.addSibling(def.getBasePluginKey(), other.getBasePluginKey(), Mask.ALL); } } } /** * Builds classloaders and verifies that all of them are correctly defined */ private static Map build(Collection defs, ClassloaderBuilder builder) { Map result = new IdentityHashMap<>(defs.size()); var classloadersByBasePluginKey = builder.build(); for (var def : defs) { var classloader = classloadersByBasePluginKey.get(def.getBasePluginKey()); if (classloader == null) { LOG.error("Fail to create classloader for plugin '{}'", def.getBasePluginKey()); } else { result.put(def, classloader); } } return result; } private static URL fileToUrl(File file) { try { return file.toURI().toURL(); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } } /** * The resources (packages) that API exposes to plugins. Other core classes (SonarQube, MyBatis, ...) * can't be accessed. *

To sum-up, these are the classes packaged in sonar-plugin-api.jar or available as * a transitive dependency of sonar-plugin-api

*/ private static Mask apiMask() { return Mask.builder() .include("org/sonar/api/") .include("org/sonarsource/api/sonarlint/") .include("org/sonar/check/") .include("net/sourceforge/pmd/") .include("com/sonarsource/plugins/license/api/") .include("org/sonarsource/sonarlint/plugin/api/") .include("org/slf4j/") // API exclusions .exclude("org/sonar/api/internal/") .build(); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginInfo.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.sonar.api.utils.MessageException; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginManifest.RequiredPlugin; import static java.util.Objects.requireNonNull; public class PluginInfo { private final String key; private String name; private File jarFile; @CheckForNull private String mainClass; @CheckForNull private Version version; @CheckForNull private Version minimalSqVersion; @CheckForNull private String basePlugin; private final Set requiredPlugins = new HashSet<>(); @CheckForNull private Version jreMinVersion; @CheckForNull private Version nodeJsMinVersion; private List dependencies = List.of(); public PluginInfo(String key) { requireNonNull(key, "Plugin key is missing from manifest"); this.key = key; this.name = key; } public PluginInfo setJarFile(File f) { this.jarFile = f; return this; } public File getJarFile() { return jarFile; } public String getKey() { return key; } public String getName() { return name; } @CheckForNull public Version getVersion() { return version; } @CheckForNull public Version getMinimalSqVersion() { return minimalSqVersion; } @CheckForNull public String getMainClass() { return mainClass; } @CheckForNull public String getBasePlugin() { return basePlugin; } public Set getRequiredPlugins() { return requiredPlugins; } @CheckForNull public Version getJreMinVersion() { return jreMinVersion; } @CheckForNull public Version getNodeJsMinVersion() { return nodeJsMinVersion; } public List getDependencies() { return dependencies; } public PluginInfo setName(@Nullable String name) { this.name = Optional.ofNullable(name).orElse(this.key); return this; } public PluginInfo setVersion(Version version) { this.version = version; return this; } public PluginInfo setMinimalSqVersion(@Nullable Version v) { this.minimalSqVersion = v; return this; } /** * Required */ public PluginInfo setMainClass(String mainClass) { this.mainClass = mainClass; return this; } public PluginInfo setBasePlugin(@Nullable String s) { this.basePlugin = s; return this; } public PluginInfo addRequiredPlugin(RequiredPlugin p) { this.requiredPlugins.add(p); return this; } private PluginInfo setMinimalJreVersion(@Nullable Version jreMinVersion) { this.jreMinVersion = jreMinVersion; return this; } private PluginInfo setMinimalNodeJsVersion(@Nullable Version nodeJsMinVersion) { this.nodeJsMinVersion = nodeJsMinVersion; return this; } public PluginInfo setDependencies(List dependencies) { this.dependencies = dependencies; return this; } /** * Find out if this plugin is compatible with a given version of SonarQube. * The version of SQ must be greater than or equal to the minimal version * needed by the plugin. */ public boolean isCompatibleWith(String implementedApi) { if (null == this.minimalSqVersion) { // no constraint defined on the plugin return true; } // Ignore patch and build numbers since this should not change API compatibility var requestedApi = Version.create(minimalSqVersion.getMajor() + "." + minimalSqVersion.getMinor()); var implementedApiVersion = Version.create(implementedApi); return implementedApiVersion.compareToIgnoreQualifier(requestedApi) >= 0; } @Override public String toString() { return "[" + key + " / " + version + "]"; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } var info = (PluginInfo) o; if (!key.equals(info.key)) { return false; } return !(version != null ? !version.equals(info.version) : (info.version != null)); } @Override public int hashCode() { var result = key.hashCode(); result = 31 * result + (version != null ? version.hashCode() : 0); return result; } public static PluginInfo create(Path jarFile) { var manifest = SonarPluginManifest.fromJar(jarFile); return create(jarFile, manifest); } static PluginInfo create(Path jarPath, SonarPluginManifest manifest) { if (StringUtils.isBlank(manifest.getKey())) { throw MessageException.of(String.format("File is not a plugin. Please delete it and restart: %s", jarPath.toAbsolutePath())); } var info = new PluginInfo(manifest.getKey()); info.setJarFile(jarPath.toFile()); info.setName(manifest.getName()); info.setMainClass(manifest.getMainClass()); var version = manifest.getVersion(); if (version != null) { info.setVersion(Version.create(version)); } info.setMinimalSqVersion(manifest.getSonarMinVersion().orElse(null)); info.setBasePlugin(manifest.getBasePluginKey()); manifest.getRequiredPlugins().forEach(info::addRequiredPlugin); info.setMinimalJreVersion(manifest.getJreMinVersion().orElse(null)); info.setMinimalNodeJsVersion(manifest.getNodeJsMinVersion().orElse(null)); info.setDependencies(manifest.getDependencies()); return info; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginInstancesLoader.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.JarURLConnection; import java.net.URI; import java.net.URL; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.zip.ZipException; import javax.annotation.CheckForNull; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.sonar.api.Plugin; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.throwFirstWithOtherSuppressed; import static org.sonarsource.sonarlint.core.commons.IOExceptionUtils.tryAndCollectIOException; /** * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating * the entry point classes as defined in manifests. It assumes that JAR files are compatible with current * environment (minimal sonarqube version, compatibility between plugins, ...): *
    *
  • server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)
  • *
  • batch loads only the plugins deployed on server (see BatchPluginRepository)
  • *
*

* Plugins have their own isolated classloader, inheriting only from API classes. * Some plugins can extend a "base" plugin, sharing the same classloader. */ public class PluginInstancesLoader implements Closeable { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"}; private final PluginClassloaderFactory classloaderFactory; private final ClassLoader baseClassLoader; private final Collection classloadersToClose = new ArrayList<>(); private final List jarFilesToClose = new ArrayList<>(); private final List filesToDelete = new ArrayList<>(); public PluginInstancesLoader() { this(new PluginClassloaderFactory()); } PluginInstancesLoader(PluginClassloaderFactory classloaderFactory) { this.classloaderFactory = classloaderFactory; this.baseClassLoader = getClass().getClassLoader(); } public Map instantiatePluginClasses(Collection plugins) { var defs = defineClassloaders(plugins.stream().collect(Collectors.toMap(PluginInfo::getKey, p -> p))); var classloaders = classloaderFactory.create(baseClassLoader, defs); this.classloadersToClose.addAll(classloaders.values()); return instantiatePluginClasses(classloaders); } /** * Defines the different classloaders to be created. Number of classloaders can be * different than number of plugins. */ Collection defineClassloaders(Map pluginsByKey) { Map classloadersByBasePlugin = new HashMap<>(); for (var info : pluginsByKey.values()) { var baseKey = basePluginKey(info, pluginsByKey); if (baseKey == null) { continue; } var def = classloadersByBasePlugin.computeIfAbsent(baseKey, PluginClassLoaderDef::new); def.addFiles(List.of(info.getJarFile())); getJarFile(info.getJarFile().toPath()).ifPresent(jarFilesToClose::add); if (!info.getDependencies().isEmpty()) { LOG.warn("Plugin '{}' embeds dependencies. This will be deprecated soon. Plugin should be updated.", info.getKey()); var tmpFolderForDeps = createTmpFolderForPluginDeps(info); for (var dependency : info.getDependencies()) { var tmpDepFile = extractDependencyInTempFolder(info, dependency, tmpFolderForDeps); def.addFiles(List.of(tmpDepFile.toFile())); filesToDelete.add(tmpDepFile); getJarFile(tmpDepFile).ifPresent(jarFilesToClose::add); } } def.addMainClass(info.getKey(), info.getMainClass()); for (var defaultSharedResource : DEFAULT_SHARED_RESOURCES) { def.getExportMaskBuilder().include(String.format("%s/%s/api/", defaultSharedResource, info.getKey())); } } return classloadersByBasePlugin.values(); } /** * SLCORE-557 Because of bug JDK-8315993 we have to somehow get access * to the underlying cached JarFile that will be also opened by the URLClassloader, and close it ourselves. */ private static Optional getJarFile(Path tmpDepFile) { try { return Optional.of(((JarURLConnection) URI.create("jar:" + tmpDepFile.toUri().toURL() + "!/").toURL().openConnection()).getJarFile()); } catch (ZipException ignore) { // For tests, we are using fake JARs, so ignore ZipException: zip file is empty return Optional.empty(); } catch (IOException e) { throw new IllegalArgumentException(e); } } private static Path createTmpFolderForPluginDeps(PluginInfo info) { try { var prefix = "sonarlint_" + info.getKey(); return Files.createTempDirectory(prefix); } catch (IOException e) { throw new IllegalStateException("Unable to create temporary directory", e); } } private static Path extractDependencyInTempFolder(PluginInfo info, String dependency, Path tempFolder) { try { var tmpDepFile = tempFolder.resolve(dependency); if (!tmpDepFile.startsWith(tempFolder + File.separator)) { throw new IOException("Entry is outside of the target dir: " + dependency); } Files.createDirectories(tmpDepFile.getParent()); extractFile(info.getJarFile().toPath(), dependency, tmpDepFile); return tmpDepFile; } catch (IOException e) { throw new IllegalStateException("Unable to extract plugin dependency: " + dependency, e); } } private static void extractFile(Path zipFile, String fileName, Path outputFile) throws IOException { try (var fileSystem = FileSystems.newFileSystem(zipFile, (ClassLoader) null)) { var fileToExtract = fileSystem.getPath(fileName); Files.copy(fileToExtract, outputFile); } } /** * Instantiates collection of {@link org.sonar.api.Plugin} according to given metadata and classloaders * * @return the instances grouped by plugin key * @throws IllegalStateException if at least one plugin can't be correctly loaded */ Map instantiatePluginClasses(Map classloaders) { // instantiate plugins Map instancesByPluginKey = new HashMap<>(); for (var entry : classloaders.entrySet()) { var def = entry.getKey(); var classLoader = entry.getValue(); // the same classloader can be used by multiple plugins for (var mainClassEntry : def.getMainClassesByPluginKey().entrySet()) { var pluginKey = mainClassEntry.getKey(); var mainClass = mainClassEntry.getValue(); try { instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).getDeclaredConstructor().newInstance()); } catch (UnsupportedClassVersionError e) { LOG.error("The plugin [{}] does not support Java {}", pluginKey, SystemUtils.JAVA_RUNTIME_VERSION, e); } catch (Throwable e) { LOG.error("Fail to instantiate class [{}] of plugin [{}]", mainClass, pluginKey, e); } } } return instancesByPluginKey; } @Override public void close() throws IOException { Queue exceptions = new LinkedList<>(); synchronized (classloadersToClose) { for (var classLoader : classloadersToClose) { if (classLoader instanceof Closeable closeableClassloader) { tryAndCollectIOException(closeableClassloader::close, exceptions); } } classloadersToClose.clear(); } synchronized (jarFilesToClose) { for (var jarFile : jarFilesToClose) { tryAndCollectIOException(jarFile::close, exceptions); } jarFilesToClose.clear(); } synchronized (filesToDelete) { for (var fileToDelete : filesToDelete) { tryAndCollectIOException(() -> FileUtils.forceDelete(fileToDelete.toFile()), exceptions); } filesToDelete.clear(); } throwFirstWithOtherSuppressed(exceptions); } /** * Get the root key of a tree of plugins. For example if plugin C depends on B, which depends on A, then * B and C must be attached to the classloader of A. The method returns A in the three cases. */ @CheckForNull static String basePluginKey(PluginInfo plugin, Map allPluginsPerKey) { var base = plugin.getKey(); var parentKey = plugin.getBasePlugin(); while (isNotEmpty(parentKey)) { var parentPlugin = allPluginsPerKey.get(parentKey); if (parentPlugin == null) { LOG.warn("Unable to find base plugin '{}' referenced by plugin '{}'", parentKey, base); return null; } base = parentPlugin.getKey(); parentKey = parentPlugin.getBasePlugin(); } return base; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginRequirementsCheckResult.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason; public class PluginRequirementsCheckResult { private final PluginInfo plugin; @CheckForNull private final SkipReason skipReason; public PluginRequirementsCheckResult(PluginInfo plugin, @Nullable SkipReason skipReason) { this.plugin = plugin; this.skipReason = skipReason; } public PluginInfo getPlugin() { return plugin; } public Optional getSkipReason() { return Optional.ofNullable(skipReason); } public boolean isSkipped() { return skipReason != null; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/SonarPluginManifest.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.nio.file.Path; import java.util.List; import java.util.Optional; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.stream.Stream; import javax.annotation.CheckForNull; import org.apache.commons.lang3.StringUtils; import org.sonarsource.sonarlint.core.commons.Version; import static java.util.Objects.requireNonNull; /** * This class loads Sonar plugin metadata from JAR manifest. */ public class SonarPluginManifest { public static final String KEY_ATTRIBUTE = "Plugin-Key"; public static final String MAIN_CLASS_ATTRIBUTE = "Plugin-Class"; public static final String NAME_ATTRIBUTE = "Plugin-Name"; public static final String VERSION_ATTRIBUTE = "Plugin-Version"; public static final String SONAR_VERSION_ATTRIBUTE = "Sonar-Version"; public static final String DEPENDENCIES_ATTRIBUTE = "Plugin-Dependencies"; public static final String REQUIRE_PLUGINS_ATTRIBUTE = "Plugin-RequirePlugins"; public static final String BASE_PLUGIN = "Plugin-Base"; public static final String JRE_MIN_VERSION = "Jre-Min-Version"; public static final String NODEJS_MIN_VERSION = "NodeJs-Min-Version"; private final String key; private final String name; private final String mainClass; private final String version; private final Optional sonarMinVersion; private final List dependencies; private final String basePluginKey; private final List requiredPlugins; private final Optional jreMinVersion; private final Optional nodeJsMinVersion; public static class RequiredPlugin { private static final Pattern PARSER = Pattern.compile("\\w+:.+"); private final String key; private final Version minimalVersion; public RequiredPlugin(String key, Version minimalVersion) { this.key = key; this.minimalVersion = minimalVersion; } public String getKey() { return key; } public Version getMinimalVersion() { return minimalVersion; } public static RequiredPlugin parse(String s) { if (!PARSER.matcher(s).matches()) { throw new IllegalArgumentException("Manifest field does not have correct format: " + s); } var fields = StringUtils.split(s, ':'); return new RequiredPlugin(fields[0], Version.create(fields[1]).removeQualifier()); } } /** * Load the manifest from a JAR file. */ public static SonarPluginManifest fromJar(Path jarPath) { try (var jar = new JarFile(jarPath.toFile())) { var manifest = jar.getManifest(); if (manifest != null) { return new SonarPluginManifest(manifest); } else { throw new IllegalStateException("No manifest in jar: " + jarPath.toAbsolutePath()); } } catch (Exception e) { throw new IllegalStateException("Error while reading plugin manifest from jar: " + jarPath.toAbsolutePath(), e); } } public SonarPluginManifest(Manifest manifest) { var attributes = manifest.getMainAttributes(); this.key = requireNonNull(attributes.getValue(KEY_ATTRIBUTE), "Plugin key is mandatory"); this.mainClass = attributes.getValue(MAIN_CLASS_ATTRIBUTE); this.name = attributes.getValue(NAME_ATTRIBUTE); this.version = attributes.getValue(VERSION_ATTRIBUTE); this.sonarMinVersion = Optional.ofNullable(attributes.getValue(SONAR_VERSION_ATTRIBUTE)).map(Version::create); this.basePluginKey = attributes.getValue(BASE_PLUGIN); var deps = attributes.getValue(DEPENDENCIES_ATTRIBUTE); this.dependencies = List.of(StringUtils.split(StringUtils.defaultString(deps), ' ')); var requires = attributes.getValue(REQUIRE_PLUGINS_ATTRIBUTE); this.requiredPlugins = Stream.of(StringUtils.split(StringUtils.defaultString(requires), ',')).map(RequiredPlugin::parse).toList(); this.jreMinVersion = Optional.ofNullable(attributes.getValue(JRE_MIN_VERSION)).map(Version::create); this.nodeJsMinVersion = Optional.ofNullable(attributes.getValue(NODEJS_MIN_VERSION)).map(Version::create); } public String getKey() { return key; } @CheckForNull public String getName() { return name; } public List getRequiredPlugins() { return requiredPlugins; } @CheckForNull public String getVersion() { return version; } public Optional getSonarMinVersion() { return sonarMinVersion; } public String getMainClass() { return mainClass; } public List getDependencies() { return dependencies; } @CheckForNull public String getBasePluginKey() { return basePluginKey; } public Optional getJreMinVersion() { return jreMinVersion; } public Optional getNodeJsMinVersion() { return nodeJsMinVersion; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/SonarPluginRequirementsChecker.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.commons.ApiVersions; import org.sonarsource.sonarlint.core.plugin.commons.DataflowBugDetection; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.UnsatisfiedRuntimeRequirement.RuntimeRequirement; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginManifest.RequiredPlugin; public class SonarPluginRequirementsChecker { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Version implementedPluginApiVersion; public SonarPluginRequirementsChecker() { this(ApiVersions.loadSonarPluginApiVersion()); } SonarPluginRequirementsChecker(org.sonar.api.utils.Version pluginApiVersion) { this.implementedPluginApiVersion = Version.create(pluginApiVersion.toString()); } /** * Attempt to read JAR manifests, load metadata, and check all requirements to ensure the plugin can be instantiated. */ public Map checkRequirements(Set pluginJarLocations, Set enabledLanguages, Version jreCurrentVersion, Optional nodeCurrentVersion, boolean enableDataflowBugDetection) { Map resultsByKey = new HashMap<>(); for (Path jarLocation : pluginJarLocations) { PluginInfo plugin; try { plugin = PluginInfo.create(jarLocation); } catch (Exception e) { LOG.error("Unable to load plugin " + jarLocation, e); continue; } if (resultsByKey.containsKey(plugin.getKey())) { throw new IllegalStateException( "Duplicate plugin key '" + plugin.getKey() + "' from '" + plugin.getJarFile() + "' and '" + resultsByKey.get(plugin.getKey()).getPlugin().getJarFile() + "'"); } resultsByKey.put(plugin.getKey(), checkIfSkippedAndPopulateReason(plugin, enabledLanguages, jreCurrentVersion, nodeCurrentVersion)); } // Second pass of checks for (PluginRequirementsCheckResult result : resultsByKey.values()) { if (!result.isSkipped()) { resultsByKey.put(result.getPlugin().getKey(), checkUnsatisfiedPluginDependency(result, resultsByKey, enableDataflowBugDetection)); } } return resultsByKey; } private PluginRequirementsCheckResult checkIfSkippedAndPopulateReason(PluginInfo plugin, Set enabledLanguages, Version jreCurrentVersion, Optional nodeCurrentVersion) { var pluginKey = plugin.getKey(); var languages = SonarPlugin.findByKey(pluginKey).map(SonarPlugin::getLanguages).orElseGet(Set::of); if (!languages.isEmpty() && enabledLanguages.stream().noneMatch(languages::contains)) { if (languages.size() > 1) { LOG.debug("Plugin '{}' is excluded because none of languages '{}' are enabled. Skip loading it.", plugin.getName(), languages.stream().map(SonarLanguage::toString).collect(Collectors.joining(","))); } else { LOG.debug("Plugin '{}' is excluded because language '{}' is not enabled. Skip loading it.", plugin.getName(), languages.iterator().next()); } return new PluginRequirementsCheckResult(plugin, new SkipReason.LanguagesNotEnabled(languages)); } if (!isCompatibleWith(plugin, implementedPluginApiVersion)) { LOG.debug("Plugin '{}' requires plugin API {} while SonarLint supports only up to {}. Skip loading it.", plugin.getName(), plugin.getMinimalSqVersion(), implementedPluginApiVersion.removeQualifier().toString()); return new PluginRequirementsCheckResult(plugin, SkipReason.IncompatiblePluginApi.INSTANCE); } var jreMinVersion = plugin.getJreMinVersion(); if (jreMinVersion != null && !jreCurrentVersion.satisfiesMinRequirement(jreMinVersion)) { LOG.debug("Plugin '{}' requires JRE {} while current is {}. Skip loading it.", plugin.getName(), jreMinVersion, jreCurrentVersion); return new PluginRequirementsCheckResult(plugin, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, jreCurrentVersion.toString(), jreMinVersion.toString())); } var nodeMinVersion = plugin.getNodeJsMinVersion(); if (nodeMinVersion != null) { if (nodeCurrentVersion.isEmpty()) { LOG.debug("Plugin '{}' requires Node.js {}. Skip loading it.", plugin.getName(), nodeMinVersion); return new PluginRequirementsCheckResult(plugin, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, null, nodeMinVersion.toString())); } else if (!nodeCurrentVersion.get().satisfiesMinRequirement(nodeMinVersion)) { LOG.debug("Plugin '{}' requires Node.js {} while current is {}. Skip loading it.", plugin.getName(), nodeMinVersion, nodeCurrentVersion.get()); return new PluginRequirementsCheckResult(plugin, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, nodeCurrentVersion.get().toString(), nodeMinVersion.toString())); } } return new PluginRequirementsCheckResult(plugin, null); } /** * Find out if this plugin is compatible with a given version of the sonar-plugin-api. * The version of the API must be greater than or equal to the minimal version * needed by the plugin. */ static boolean isCompatibleWith(PluginInfo plugin, Version implementedApiVersion) { var sonarMinVersion = plugin.getMinimalSqVersion(); if (sonarMinVersion == null) { // no constraint defined on the plugin return true; } // Ignore patch and build numbers since this should not change API compatibility var requestedApi = Version.create(sonarMinVersion.getMajor() + "." + sonarMinVersion.getMinor()); return implementedApiVersion.satisfiesMinRequirement(requestedApi); } private static PluginRequirementsCheckResult checkUnsatisfiedPluginDependency(PluginRequirementsCheckResult currentResult, Map currentResultsByKey, boolean enableDataflowBugDetection) { var plugin = currentResult.getPlugin(); for (RequiredPlugin required : plugin.getRequiredPlugins()) { if ("license".equals(required.getKey())) { continue; } var depInfo = currentResultsByKey.get(required.getKey()); // We could possibly have a problem with transitive dependencies, since we evaluate in no specific order. // A -> B -> C // If C is skipped, then B should be skipped, then A should be skipped // If we evaluate A before B, then A might be wrongly included // But I'm not aware of such case in real life. if (depInfo == null || depInfo.isSkipped()) { return processUnsatisfiedDependency(currentResult.getPlugin(), required.getKey()); } } var basePluginKey = plugin.getBasePlugin(); if (basePluginKey != null && checkForPluginSkipped(currentResultsByKey.get(basePluginKey))) { return processUnsatisfiedDependency(currentResult.getPlugin(), basePluginKey); } if (DataflowBugDetection.PLUGIN_ALLOW_LIST.contains(plugin.getKey())) { // Workaround for SLCORE-667 // dbd and dbdpythonfrontend require Python to be working if (!enableDataflowBugDetection) { LOG.debug("DBD feature disabled. Skip loading plugin '{}'.", plugin.getName()); return new PluginRequirementsCheckResult(plugin, SkipReason.UnsupportedFeature.INSTANCE); } var pythonPluginResult = currentResultsByKey.get(SonarPlugin.PYTHON.getKey()); if (checkForPluginSkipped(pythonPluginResult)) { return processUnsatisfiedDependency(currentResult.getPlugin(), SonarPlugin.PYTHON.getKey()); } } return currentResult; } private static boolean checkForPluginSkipped(@Nullable PluginRequirementsCheckResult plugin) { return plugin == null || plugin.isSkipped(); } private static PluginRequirementsCheckResult processUnsatisfiedDependency(PluginInfo plugin, String pluginKeyDependency) { LOG.debug("Plugin '{}' dependency on '{}' is unsatisfied. Skip loading it.", plugin.getName(), pluginKeyDependency); return new PluginRequirementsCheckResult(plugin, new SkipReason.UnsatisfiedDependency(pluginKeyDependency)); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/loading/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.commons.loading; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.commons; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/ConfigurationBridge.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import java.util.Optional; import org.sonar.api.config.Configuration; import org.sonar.api.config.Settings; /** * Used to help migration from {@link Settings} to {@link Configuration} */ public class ConfigurationBridge implements Configuration { private final Settings settings; public ConfigurationBridge(Settings settings) { this.settings = settings; } @Override public Optional get(String key) { return Optional.ofNullable(settings.getString(key)); } @Override public boolean hasKey(String key) { return settings.hasKey(key); } @Override public String[] getStringArray(String key) { return settings.getStringArray(key); } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/MapSettings.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.CheckForNull; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.sonar.api.config.Configuration; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.Settings; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toUnmodifiableMap; import static org.apache.commons.lang3.StringUtils.trim; import static org.sonarsource.sonarlint.core.plugin.commons.MultivalueProperty.parseAsCsv; public class MapSettings extends Settings { private final Map props; private final ConfigurationBridge configurationBridge; private final PropertyDefinitions definitions; // For testing public MapSettings(Map props) { this(new PropertyDefinitions(System2.INSTANCE), props); } public MapSettings(PropertyDefinitions definitions, Map props) { this.props = props.entrySet().stream() .collect( toUnmodifiableMap(e -> definitions.validKey(e.getKey()), e -> trim(e.getValue()))); this.definitions = definitions; configurationBridge = new ConfigurationBridge(this); } protected Optional get(String key) { return Optional.ofNullable(props.get(key)); } public Map getProperties() { return props; } /** * The value that overrides the default value. It * may be encrypted with a secret key. Use {@link #getString(String)} to get * the effective and decrypted value. * * @since 6.1 */ public Optional getRawString(String key) { return get(definitions.validKey(requireNonNull(key))); } /** * All the property definitions declared by core and plugins. */ public PropertyDefinitions getDefinitions() { return definitions; } /** * The definition related to the specified property. It may * be empty. * * @since 6.1 */ public Optional getDefinition(String key) { return Optional.ofNullable(definitions.get(key)); } /** * @return {@code true} if the property has a non-default value, else {@code false}. */ @Override public boolean hasKey(String key) { return getRawString(key).isPresent(); } @CheckForNull public String getDefaultValue(String key) { return definitions.getDefaultValue(key); } public boolean hasDefaultValue(String key) { return StringUtils.isNotEmpty(getDefaultValue(key)); } /** * The effective value of the specified property. Can return * {@code null} if the property is not set and has no * defined default value. *

* If the property is encrypted with a secret key, * then the returned value is decrypted. *

* * @throws IllegalStateException if value is encrypted but fails to be decrypted. */ @CheckForNull @Override public String getString(String key) { var effectiveKey = definitions.validKey(key); // default values cannot be encrypted, so return value as-is. return getRawString(effectiveKey) .orElseGet(() -> getDefaultValue(effectiveKey)); } /** * Effective value as boolean. It is {@code false} if {@link #getString(String)} * does not return {@code "true"}, even if it's not a boolean representation. * * @return {@code true} if the effective value is {@code "true"}, else {@code false}. */ @Override public boolean getBoolean(String key) { var value = getString(key); return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value); } /** * Effective value as {@code int}. * * @return the value as {@code int}. If the property does not have value nor default value, then {@code 0} is returned. * @throws NumberFormatException if value is not empty and is not a parsable integer */ @Override public int getInt(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { return Integer.parseInt(value); } return 0; } /** * Effective value as {@code long}. * * @return the value as {@code long}. If the property does not have value nor default value, then {@code 0L} is returned. * @throws NumberFormatException if value is not empty and is not a parsable {@code long} */ @Override public long getLong(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { return Long.parseLong(value); } return 0L; } /** * Effective value as {@link Date}, without time fields. Format is {@link DateUtils#DATE_FORMAT}. * * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned. * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATE_FORMAT}. */ @CheckForNull @Override public Date getDate(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { return DateUtils.parseDate(value); } return null; } /** * Effective value as {@link Date}, with time fields. Format is {@link DateUtils#DATETIME_FORMAT}. * * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned. * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATETIME_FORMAT}. */ @CheckForNull @Override public Date getDateTime(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { return DateUtils.parseDateTime(value); } return null; } /** * Effective value as {@code Float}. * * @return the value as {@code Float}. If the property does not have value nor default value, then {@code null} is returned. * @throws NumberFormatException if value is not empty and is not a parsable number */ @CheckForNull @Override public Float getFloat(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { try { return Float.valueOf(value); } catch (NumberFormatException e) { throw new IllegalStateException(String.format("The property '%s' is not a float value", key)); } } return null; } /** * Effective value as {@code Double}. * * @return the value as {@code Double}. If the property does not have value nor default value, then {@code null} is returned. * @throws NumberFormatException if value is not empty and is not a parsable number */ @CheckForNull @Override public Double getDouble(String key) { var value = getString(key); if (StringUtils.isNotEmpty(value)) { try { return Double.valueOf(value); } catch (NumberFormatException e) { throw new IllegalStateException(String.format("The property '%s' is not a double value", key)); } } return null; } /** * Value is split by comma and trimmed. Never returns null. *
* Examples : *
    *
  • "one,two,three " -> ["one", "two", "three"]
  • *
  • " one, two, three " -> ["one", "two", "three"]
  • *
  • "one, , three" -> ["one", "", "three"]
  • *
*/ @Override public String[] getStringArray(String key) { var effectiveKey = definitions.validKey(key); var def = getDefinition(effectiveKey); if ((def.isPresent()) && (def.get().multiValues())) { var value = getString(key); if (value == null) { return ArrayUtils.EMPTY_STRING_ARRAY; } return parseAsCsv(effectiveKey, value); } return getStringArrayBySeparator(key, ","); } /** * Value is split by carriage returns. * * @return non-null array of lines. The line termination characters are excluded. * @since 3.2 */ @Override public String[] getStringLines(String key) { var value = getString(key); if (StringUtils.isEmpty(value)) { return new String[0]; } return value.split("\r?\n|\r", -1); } /** * Value is split and trimmed. */ @Override public String[] getStringArrayBySeparator(String key, String separator) { var value = getString(key); if (value != null) { var strings = StringUtils.splitByWholeSeparator(value, separator); var result = new String[strings.length]; for (var index = 0; index < strings.length; index++) { result[index] = trim(strings[index]); } return result; } return ArrayUtils.EMPTY_STRING_ARRAY; } @Override public List getKeysStartingWith(String prefix) { return getProperties().keySet().stream() .filter(key -> Strings.CS.startsWith(key, prefix)) .toList(); } /** * @return a {@link Configuration} proxy on top of this existing {@link Settings} implementation. Changes are reflected in the {@link Configuration} object. * @since 6.5 */ public Configuration asConfig() { return configurationBridge; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/PluginContextImpl.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import java.util.Objects; import org.sonar.api.Plugin; import org.sonar.api.config.Configuration; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; public class PluginContextImpl extends Plugin.Context { private final Configuration bootConfiguration; private PluginContextImpl(Builder builder) { super(builder.sonarRuntime); this.bootConfiguration = builder.bootConfiguration; } @Override public Configuration getBootConfiguration() { return bootConfiguration; } @Override public SonarLintRuntime getRuntime() { return (SonarLintRuntime) super.getRuntime(); } public static class Builder { private SonarLintRuntime sonarRuntime; private Configuration bootConfiguration; /** * Required. * @see SonarLintRuntimeImpl * @return this */ public Builder setSonarRuntime(SonarLintRuntime r) { this.sonarRuntime = r; return this; } /** * Required. * @return this */ public Builder setBootConfiguration(Configuration c) { this.bootConfiguration = c; return this; } public Plugin.Context build() { Objects.requireNonNull(bootConfiguration, "bootConfiguration is mandatory"); return new PluginContextImpl(this); } } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/SonarLintRuntimeImpl.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import org.sonar.api.SonarEdition; import org.sonar.api.SonarProduct; import org.sonar.api.SonarQubeSide; import org.sonar.api.utils.Version; import org.sonarsource.sonarlint.plugin.api.SonarLintRuntime; import static java.util.Objects.requireNonNull; public class SonarLintRuntimeImpl implements SonarLintRuntime { private final Version sonarPluginApiVersion; private final Version sonarLintPluginApiVersion; private final long clientPid; public SonarLintRuntimeImpl(Version sonarPluginApiVersion, Version sonarLintPluginApiVersion, long clientPid) { this.clientPid = clientPid; this.sonarPluginApiVersion = requireNonNull(sonarPluginApiVersion); this.sonarLintPluginApiVersion = sonarLintPluginApiVersion; } @Override public Version getApiVersion() { return sonarPluginApiVersion; } @Override public Version getSonarLintPluginApiVersion() { return sonarLintPluginApiVersion; } @Override public SonarProduct getProduct() { return SonarProduct.SONARLINT; } @Override public SonarQubeSide getSonarQubeSide() { throw new UnsupportedOperationException("Can only be called in SonarQube"); } @Override public SonarEdition getEdition() { throw new UnsupportedOperationException("Can only be called in SonarQube"); } @Override public long getClientPid() { return clientPid; } } ================================================ FILE: backend/plugin-commons/src/main/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/package-info.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/plugin-commons/src/test/java/com/sonarsource/plugins/license/api/LicensedPluginRegistrationTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.sonarsource.plugins.license.api; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class LicensedPluginRegistrationTests { @Test void testBuilder() { var underTest = LicensedPluginRegistration.forPlugin("xoo"); assertThat(underTest.getPluginKey()).isEqualTo("xoo"); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/ApiVersionsTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.net.URI; import org.junit.jupiter.api.Test; import org.sonar.api.utils.Version; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ApiVersionsTests { @Test void can_load_sonar_plugin_api_version_from_embedded_resource() { var version = ApiVersions.loadSonarPluginApiVersion(); assertThat(version).isNotNull(); assertThat(version.isGreaterThanOrEqual(Version.create(8, 5))).isTrue(); } @Test void can_load_sonarlint_plugin_api_version_from_embedded_resource() { var version = ApiVersions.loadSonarLintPluginApiVersion(); assertThat(version).isNotNull(); assertThat(version.isGreaterThanOrEqual(Version.create(5, 4))).isTrue(); } @Test void should_throw_an_exception_if_resource_does_not_exist() { var throwable = catchThrowable(() -> ApiVersions.loadVersion(null, "wrongPath")); assertThat(throwable).hasMessage("Can not load wrongPath from classpath"); } @Test void should_throw_an_exception_if_resource_can_not_be_loaded() { var throwable = catchThrowable(() -> ApiVersions.loadVersion(URI.create("file://wrong").toURL(), "wrongPath")); assertThat(throwable).hasMessage("Can not load wrongPath from classpath"); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/ExtensionUtilsTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import org.junit.jupiter.api.Test; import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.ScannerSide; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.server.ServerSide; import org.sonarsource.api.sonarlint.SonarLintSide; import static org.assertj.core.api.Assertions.assertThat; class ExtensionUtilsTests { @Test void shouldBeBatchInstantiationStrategy() { assertThat(ExtensionUtils.isInstantiationStrategy(DefaultScannerService.class, InstantiationStrategy.PER_BATCH)).isFalse(); assertThat(ExtensionUtils.isInstantiationStrategy(new DefaultScannerService(), InstantiationStrategy.PER_BATCH)).isFalse(); } @Test void shouldBeProjectInstantiationStrategy() { assertThat(ExtensionUtils.isInstantiationStrategy(DefaultScannerService.class, InstantiationStrategy.PER_PROJECT)).isTrue(); assertThat(ExtensionUtils.isInstantiationStrategy(new DefaultScannerService(), InstantiationStrategy.PER_PROJECT)).isTrue(); } @Test void testIsSonarLintSide() { assertThat(ExtensionUtils.isSonarLintSide(ScannerService.class)).isFalse(); assertThat(ExtensionUtils.isSonarLintSide(ServerService.class)).isFalse(); assertThat(ExtensionUtils.isSonarLintSide(new ServerService())).isFalse(); assertThat(ExtensionUtils.isSonarLintSide(new WebServerService())).isFalse(); assertThat(ExtensionUtils.isSonarLintSide(new ComputeEngineService())).isFalse(); assertThat(ExtensionUtils.isSonarLintSide(new DefaultSonarLintService())).isTrue(); } @ScannerSide @InstantiationStrategy(InstantiationStrategy.PER_BATCH) public static class ScannerService { } @ScannerSide public static class DefaultScannerService { } @SonarLintSide public static class DefaultSonarLintService { } @ServerSide public static class ServerService { } @ServerSide public static class WebServerService { } @ComputeEngineSide public static class ComputeEngineService { } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/MultivaluePropertyTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.util.Random; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.sonarsource.sonarlint.core.plugin.commons.MultivalueProperty.parseAsCsv; import static org.sonarsource.sonarlint.core.plugin.commons.MultivalueProperty.trimFieldsAndRemoveEmptyFields; import static org.sonarsource.sonarlint.core.plugin.commons.Utils.randomAlphanumeric; class MultivaluePropertyTests { private static final String[] EMPTY_STRING_ARRAY = {}; @ParameterizedTest @MethodSource("testParseAsCsv") void parseAsCsv_for_coverage(String value, String[] expected) { assertThat(parseAsCsv("key", value)) .isEqualTo(parseAsCsv("key", value, identity())) .isEqualTo(expected); } @Test void parseAsCsv_fails_with_ISE_if_value_can_not_be_parsed() { assertThatThrownBy(() -> parseAsCsv("multi", "\"a ,b")) .isInstanceOf(IllegalStateException.class) .hasMessage("Property: 'multi' doesn't contain a valid CSV value: '\"a ,b'"); } public static Stream testParseAsCsv() { return Stream.of( Arguments.of("", EMPTY_STRING_ARRAY), Arguments.of("a", arrayOf("a")), Arguments.of(" a", arrayOf("a")), Arguments.of("a ", arrayOf("a")), Arguments.of(" a, b", arrayOf("a", "b")), Arguments.of("a,b ", arrayOf("a", "b")), Arguments.of("a,,,b,c,,d", arrayOf("a", "b", "c", "d")), Arguments.of("a,\n\tb,\n c,\n d\n", arrayOf("a", "b", "c", "d")), Arguments.of("a\n\tb\n c,\n d\n", arrayOf("a\nb\nc", "d")), Arguments.of("\na\n\tb\n c,\n d\n", arrayOf("a\nb\nc", "d")), Arguments.of("a,\n,\nb", arrayOf("a", "b")), Arguments.of(" , \n ,, \t", EMPTY_STRING_ARRAY), Arguments.of("\" a\"", arrayOf(" a")), Arguments.of("\",\"", arrayOf(",")), // escaped quote in quoted field Arguments.of("\"\"\"\"", arrayOf("\""))); } private static String[] arrayOf(String... strs) { return strs; } @Test void trimFieldsAndRemoveEmptyFields_throws_NPE_if_arg_is_null() { assertThatThrownBy(() -> trimFieldsAndRemoveEmptyFields(null)) .isInstanceOf(NullPointerException.class); } @ParameterizedTest @MethodSource("plains") void trimFieldsAndRemoveEmptyFields_ignores_EmptyFields(String str) { assertThat(trimFieldsAndRemoveEmptyFields("")).isEmpty(); assertThat(trimFieldsAndRemoveEmptyFields(str)).isEqualTo(str); assertThat(trimFieldsAndRemoveEmptyFields(',' + str)).isEqualTo(str); assertThat(trimFieldsAndRemoveEmptyFields(str + ',')).isEqualTo(str); assertThat(trimFieldsAndRemoveEmptyFields(",,," + str)).isEqualTo(str); assertThat(trimFieldsAndRemoveEmptyFields(str + ",,,")).isEqualTo(str); assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str)).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str)).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str)).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields("," + str + ",,," + str)).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(",,," + str + ",,," + str)).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',')).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str + ",")).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ",,," + str + ",,")).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str + ',')).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(",," + str + ',' + str + ',')).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ",," + str + ',')).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(',' + str + ',' + str + ",,")).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(",,," + str + ",,," + str + ",,")).isEqualTo(str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',' + str)).isEqualTo(str + ',' + str + ',' + str); assertThat(trimFieldsAndRemoveEmptyFields(str + ',' + str + ',' + str)).isEqualTo(str + ',' + str + ',' + str); } public static Object[][] plains() { return new Object[][] { {randomAlphanumeric(1)}, {randomAlphanumeric(2)}, {randomAlphanumeric(3 + new Random().nextInt(5))} }; } @ParameterizedTest @MethodSource("emptyAndtrimmable") void trimFieldsAndRemoveEmptyFields_ignores_empty_fields_and_trims_fields(String empty, String trimmable) { String expected = trimmable.trim(); assertThat(empty.trim()).isEmpty(); assertThat(trimFieldsAndRemoveEmptyFields(trimmable)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ",," + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + trimmable)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ",," + trimmable)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + trimmable + ',' + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ",," + trimmable + ",,," + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + empty + ',' + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ",," + empty + ",,," + empty)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ',' + empty + ',' + trimmable)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(empty + ",,,," + empty + ",," + trimmable)).isEqualTo(expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + ',' + trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected + ',' + expected); assertThat(trimFieldsAndRemoveEmptyFields(trimmable + "," + trimmable + ',' + trimmable)).isEqualTo(expected + ',' + expected + ',' + expected); } @Test void trimAccordingToStringTrim() { String str = randomAlphanumeric(4); for (int i = 0; i <= ' '; i++) { String prefixed = (char) i + str; String suffixed = (char) i + str; String both = (char) i + str + (char) i; assertThat(trimFieldsAndRemoveEmptyFields(prefixed)).isEqualTo(prefixed.trim()); assertThat(trimFieldsAndRemoveEmptyFields(suffixed)).isEqualTo(suffixed.trim()); assertThat(trimFieldsAndRemoveEmptyFields(both)).isEqualTo(both.trim()); } } public static Object[][] emptyAndtrimmable() { Random random = new Random(); String oneEmpty = randomTrimmedChars(1, random); String twoEmpty = randomTrimmedChars(2, random); String threePlusEmpty = randomTrimmedChars(3 + random.nextInt(5), random); String onePlusEmpty = randomTrimmedChars(1 + random.nextInt(5), random); String plain = randomAlphanumeric(1); String plainWithtrimmable = randomAlphanumeric(2) + onePlusEmpty + randomAlphanumeric(3); String quotedWithSeparator = '"' + randomAlphanumeric(3) + ',' + randomAlphanumeric(2) + '"'; String quotedWithDoubleSeparator = '"' + randomAlphanumeric(3) + ",," + randomAlphanumeric(2) + '"'; String quotedWithtrimmable = '"' + randomAlphanumeric(3) + onePlusEmpty + randomAlphanumeric(2) + '"'; String[] empties = {oneEmpty, twoEmpty, threePlusEmpty}; String[] strings = {plain, plainWithtrimmable, onePlusEmpty + plain, plain + onePlusEmpty, onePlusEmpty + plain + onePlusEmpty, onePlusEmpty + plainWithtrimmable, plainWithtrimmable + onePlusEmpty, onePlusEmpty + plainWithtrimmable + onePlusEmpty, onePlusEmpty + quotedWithSeparator, quotedWithSeparator + onePlusEmpty, onePlusEmpty + quotedWithSeparator + onePlusEmpty, onePlusEmpty + quotedWithDoubleSeparator, quotedWithDoubleSeparator + onePlusEmpty, onePlusEmpty + quotedWithDoubleSeparator + onePlusEmpty, onePlusEmpty + quotedWithtrimmable, quotedWithtrimmable + onePlusEmpty, onePlusEmpty + quotedWithtrimmable + onePlusEmpty }; Object[][] res = new Object[empties.length * strings.length][2]; int i = 0; for (String empty : empties) { for (String string : strings) { res[i][0] = empty; res[i][1] = string; i++; } } return res; } @ParameterizedTest @MethodSource("emptys") void trimFieldsAndRemoveEmptyFields_quotes_allow_to_preserve_fields(String empty) { String quotedEmpty = '"' + empty + '"'; assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty)).isEqualTo(quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(',' + quotedEmpty)).isEqualTo(quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',')).isEqualTo(quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(',' + quotedEmpty + ',')).isEqualTo(quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',' + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ",," + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty); assertThat(trimFieldsAndRemoveEmptyFields(quotedEmpty + ',' + quotedEmpty + ',' + quotedEmpty)).isEqualTo(quotedEmpty + ',' + quotedEmpty + ',' + quotedEmpty); } public static Object[][] emptys() { Random random = new Random(); return new Object[][] { {randomTrimmedChars(1, random)}, {randomTrimmedChars(2, random)}, {randomTrimmedChars(3 + random.nextInt(5), random)} }; } @Test void trimFieldsAndRemoveEmptyFields_supports_escaped_quote_in_quotes() { assertThat(trimFieldsAndRemoveEmptyFields("\"f\"\"oo\"")).isEqualTo("\"f\"\"oo\""); assertThat(trimFieldsAndRemoveEmptyFields("\"f\"\"oo\",\"bar\"\"\"")).isEqualTo("\"f\"\"oo\",\"bar\"\"\""); } @Test void trimFieldsAndRemoveEmptyFields_does_not_fail_on_unbalanced_quotes() { assertThat(trimFieldsAndRemoveEmptyFields("\"")).isEqualTo("\""); assertThat(trimFieldsAndRemoveEmptyFields("\"foo")).isEqualTo("\"foo"); assertThat(trimFieldsAndRemoveEmptyFields("foo\"")).isEqualTo("foo\""); assertThat(trimFieldsAndRemoveEmptyFields("\"foo\",\"")).isEqualTo("\"foo\",\""); assertThat(trimFieldsAndRemoveEmptyFields("\",\"foo\"")).isEqualTo("\",\"foo\""); assertThat(trimFieldsAndRemoveEmptyFields("\"foo\",\", ")).isEqualTo("\"foo\",\", "); assertThat(trimFieldsAndRemoveEmptyFields(" a ,,b , c, \"foo\",\" ")).isEqualTo("a,b,c,\"foo\",\" "); assertThat(trimFieldsAndRemoveEmptyFields("\" a ,,b , c, ")).isEqualTo("\" a ,,b , c, "); } private static final char[] SOME_PRINTABLE_TRIMMABLE_CHARS = { ' ', '\t', '\n', '\r' }; /** * Result of randomTrimmedChars being used as arguments to JUnit test method through the DataProvider feature, they * are printed to surefire report. Some of those chars breaks the parsing of the surefire report during sonar analysis. * Therefor, we only use a subset of the trimmable chars. */ private static String randomTrimmedChars(int length, Random random) { char[] chars = new char[length]; for (int i = 0; i < chars.length; i++) { chars[i] = SOME_PRINTABLE_TRIMMABLE_CHARS[random.nextInt(SOME_PRINTABLE_TRIMMABLE_CHARS.length)]; } return new String(chars); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/SkipReasonTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import java.util.List; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.IncompatiblePluginApi; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.LanguagesNotEnabled; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.UnsatisfiedDependency; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.UnsatisfiedRuntimeRequirement; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.UnsatisfiedRuntimeRequirement.RuntimeRequirement; import static org.assertj.core.api.Assertions.assertThat; class SkipReasonTests { @Test void testLanguageNotEnabled_getters_equals_hashcode_tostring() { var underTest = new LanguagesNotEnabled(List.of(SonarLanguage.JAVA)); // Getters assertThat(underTest.getNotEnabledLanguages()) .containsExactly(SonarLanguage.JAVA); assertThat(underTest) // Equals .isEqualTo(underTest) .isNotEqualTo(IncompatiblePluginApi.INSTANCE) .isNotEqualTo(new LanguagesNotEnabled(List.of(SonarLanguage.JS))) .isEqualTo(new LanguagesNotEnabled(List.of(SonarLanguage.JAVA))) // HashCode .hasSameHashCodeAs(underTest) .hasSameHashCodeAs(new LanguagesNotEnabled(List.of(SonarLanguage.JAVA))) // To String .hasToString("LanguagesNotEnabled [languages=[JAVA]]"); } @Test void testUnsatisfiedDependency_getters_equals_hashcode_tostring() { var underTest = new UnsatisfiedDependency("foo"); // Getters assertThat(underTest.getDependencyKey()).isEqualTo("foo"); assertThat(underTest) // Equals .isEqualTo(underTest) .isNotEqualTo(IncompatiblePluginApi.INSTANCE) .isNotEqualTo(new UnsatisfiedDependency("bar")) .isEqualTo(new UnsatisfiedDependency("foo")) // HashCode .hasSameHashCodeAs(underTest) .hasSameHashCodeAs(new UnsatisfiedDependency("foo")) // To String .hasToString("UnsatisfiedDependency [dependencyKey=foo]"); } @Test void testUnsatisfiedRuntimeRequirement_getters_equals_hashcode_tostring() { var underTest = new UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "1.0", "2.0"); // Getters assertThat(underTest.getMinVersion()).isEqualTo("2.0"); assertThat(underTest.getCurrentVersion()).isEqualTo("1.0"); assertThat(underTest) // Equals .isEqualTo(underTest) .isNotEqualTo(IncompatiblePluginApi.INSTANCE) .isNotEqualTo(new UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, "1.0", "2.0")) .isNotEqualTo(new UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "1.0", "1.0")) .isNotEqualTo(new UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "2.0", "1.0")) .isEqualTo(new UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "1.0", "2.0")) // HashCode .hasSameHashCodeAs(underTest) .hasSameHashCodeAs(new UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "1.0", "2.0")) // To String .hasToString("UnsatisfiedRuntimeRequirement [runtime=JRE, currentVersion=1.0, minVersion=2.0]"); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/Utils.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons; import static org.apache.commons.lang3.RandomStringUtils.insecure; public class Utils { private Utils() { // utility class } public static String randomAlphanumeric(int count) { return insecure().nextAlphanumeric(count); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/container/ComponentKeysTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; class ComponentKeysTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); ComponentKeys keys = new ComponentKeys(); @Test void generate_key_of_object() { assertThat(keys.of(FakeComponent.class)).isEqualTo(FakeComponent.class); } @Test void generate_key_of_instance() { assertThat((String) keys.of(new FakeComponent())).endsWith("-org.sonarsource.sonarlint.core.plugin.commons.container.ComponentKeysTests.FakeComponent-fake"); } @Test void generate_key_of_class() { assertThat(keys.ofClass(FakeComponent.class)).endsWith("-org.sonarsource.sonarlint.core.plugin.commons.container.ComponentKeysTests.FakeComponent"); } @Test void should_log_warning_if_toString_is_not_overridden() { SonarLintLogger log = mock(SonarLintLogger.class); keys.of(new Object(), log); verifyNoInteractions(log); // only on non-first runs, to avoid false-positives on singletons keys.of(new Object(), log); verify(log).warn(startsWith("Bad component key")); } @Test void should_generate_unique_key_when_toString_is_not_overridden() { Object key = keys.of(new WrongToStringImpl()); assertThat(key).isNotEqualTo(WrongToStringImpl.KEY); Object key2 = keys.of(new WrongToStringImpl()); assertThat(key2).isNotEqualTo(key); } static class FakeComponent { @Override public String toString() { return "fake"; } } static class WrongToStringImpl { static final String KEY = "my.Component@123a"; @Override public String toString() { return KEY; } } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/container/LazyUnlessStartableStrategyTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import static org.assertj.core.api.Assertions.assertThat; class LazyUnlessStartableStrategyTests { private final LazyUnlessStartableStrategy postProcessor = new LazyUnlessStartableStrategy(); @Test void sets_all_beans_lazy() { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition("bean1", new RootBeanDefinition()); assertThat(beanFactory.getBeanDefinition("bean1").isLazyInit()).isFalse(); postProcessor.postProcessBeanFactory(beanFactory); assertThat(beanFactory.getBeanDefinition("bean1").isLazyInit()).isTrue(); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/container/PriorityBeanFactoryTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import jakarta.annotation.Priority; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class PriorityBeanFactoryTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final DefaultListableBeanFactory parentBeanFactory = new PriorityBeanFactory(); private final DefaultListableBeanFactory beanFactory = new PriorityBeanFactory(); @BeforeEach public void setUp() { // needed to support autowiring with @Inject beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); // needed to read @Priority beanFactory.setDependencyComparator(new AnnotationAwareOrderComparator()); beanFactory.setParentBeanFactory(parentBeanFactory); } @Test void give_priority_to_child_container() { parentBeanFactory.registerBeanDefinition("A1", new RootBeanDefinition(A1.class)); beanFactory.registerBeanDefinition("A2", new RootBeanDefinition(A2.class)); beanFactory.registerBeanDefinition("B", new RootBeanDefinition(B.class)); assertThat(beanFactory.getBean(B.class).dep.getClass()).isEqualTo(A2.class); } @Test void follow_priority_annotations() { parentBeanFactory.registerBeanDefinition("A3", new RootBeanDefinition(A3.class)); beanFactory.registerBeanDefinition("A1", new RootBeanDefinition(A1.class)); beanFactory.registerBeanDefinition("A2", new RootBeanDefinition(A2.class)); beanFactory.registerBeanDefinition("B", new RootBeanDefinition(B.class)); assertThat(beanFactory.getBean(B.class).dep.getClass()).isEqualTo(A3.class); } @Test void throw_NoUniqueBeanDefinitionException_if_cant_find_single_bean_with_higher_priority() { beanFactory.registerBeanDefinition("A1", new RootBeanDefinition(A1.class)); beanFactory.registerBeanDefinition("A2", new RootBeanDefinition(A2.class)); beanFactory.registerBeanDefinition("B", new RootBeanDefinition(B.class)); assertThatThrownBy(() -> beanFactory.getBean(B.class)) .hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); } private static class B { private final A dep; public B(A dep) { this.dep = dep; } } private interface A { } private static class A1 implements A { } private static class A2 implements A { } @Priority(1) private static class A3 implements A { } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/container/SpringComponentContainerTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import java.util.Arrays; import java.util.List; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.Property; import org.sonar.api.Startable; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinitions; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; class SpringComponentContainerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void should_stop_after_failing() { ApiStartable startStop = new ApiStartable(); SpringComponentContainer container = new SpringComponentContainer() { @Override public void doBeforeStart() { add(startStop); } @Override public void doAfterStart() { getComponentByType(ApiStartable.class); throw new IllegalStateException("doBeforeStart"); } }; assertThrows(IllegalStateException.class, () -> container.execute(null)); assertThat(startStop.start).isOne(); assertThat(startStop.stop).isOne(); } @Test void add_registers_instance_with_toString() { SpringComponentContainer container = new SimpleContainer(new ToString("a"), new ToString("b")); container.startComponents(); assertThat(container.context.getBeanDefinitionNames()) .contains( this.getClass().getClassLoader() + "-org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests.ToString-a", this.getClass().getClassLoader() + "-org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests.ToString-b"); assertThat(container.getComponentsByType(ToString.class)).hasSize(2); } @Test void add_registers_class_with_classloader_and_fqcn() { SpringComponentContainer container = new SimpleContainer(A.class, B.class); container.startComponents(); assertThat(container.context.getBeanDefinitionNames()) .contains( this.getClass().getClassLoader() + "-org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests.A", this.getClass().getClassLoader() + "-org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests.B"); assertThat(container.getComponentByType(A.class)).isNotNull(); assertThat(container.getComponentByType(B.class)).isNotNull(); } @Test void get_optional_component_by_type_should_return_correctly() { SpringComponentContainer container = new SpringComponentContainer(); container.add(A.class); container.startComponents(); assertThat(container.getOptionalComponentByType(A.class)).containsInstanceOf(A.class); assertThat(container.getOptionalComponentByType(B.class)).isEmpty(); } @Test void createChild_method_should_spawn_a_child_container() { SpringComponentContainer parent = new SpringComponentContainer(); SpringComponentContainer child = parent.createChild(); assertThat(child).isNotEqualTo(parent); assertThat(child.parent).isEqualTo(parent); assertThat(parent.children).contains(child); } @Test void get_component_by_type_should_throw_exception_when_type_does_not_exist() { SpringComponentContainer container = new SpringComponentContainer(); container.add(A.class); container.startComponents(); assertThatThrownBy(() -> container.getComponentByType(B.class)) .isInstanceOf(IllegalStateException.class) .hasMessage("Unable to load component class org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests$B"); } @Test void should_throw_start_exception_if_stop_also_throws_exception() { ErrorStopClass errorStopClass = new ErrorStopClass(); SpringComponentContainer container = new SpringComponentContainer() { @Override public void doBeforeStart() { add(errorStopClass); } @Override public void doAfterStart() { getComponentByType(ErrorStopClass.class); throw new IllegalStateException("doBeforeStart"); } }; assertThrows(IllegalStateException.class, () -> container.execute(null)); assertThat(errorStopClass.stopped).isTrue(); } @Test void addExtension_supports_extensions_without_annotations() { SpringComponentContainer container = new SimpleContainer(A.class, B.class); container.addExtension("", ExtensionWithMultipleConstructorsAndNoAnnotations.class); container.startComponents(); assertThat(container.getComponentByType(ExtensionWithMultipleConstructorsAndNoAnnotations.class).gotBothArgs).isTrue(); } @Test void addExtension_supports_extension_instances_without_annotations() { SpringComponentContainer container = new SpringComponentContainer(); container.addExtension("", new ExtensionWithMultipleConstructorsAndNoAnnotations(new A())); container.startComponents(); assertThat(container.getComponentByType(ExtensionWithMultipleConstructorsAndNoAnnotations.class)).isNotNull(); } @Test void addExtension_resolves_iterables() { List> classes = Arrays.asList(A.class, B.class); SpringComponentContainer container = new SpringComponentContainer(); container.addExtension("", classes); container.startComponents(); assertThat(container.getComponentByType(A.class)).isNotNull(); assertThat(container.getComponentByType(B.class)).isNotNull(); } @Test void declareExtension_adds_property() { SpringComponentContainer container = new SpringComponentContainer(); container.addExtension("myPlugin", A.class); container.startComponents(); PropertyDefinitions propertyDefinitions = container.getComponentByType(PropertyDefinitions.class); PropertyDefinition propertyDefinition = propertyDefinitions.get("k"); assertThat(propertyDefinition.key()).isEqualTo("k"); assertThat(propertyDefinitions.getCategory("k")).isEmpty(); } @Test void stop_should_stop_children() { SpringComponentContainer parent = new SpringComponentContainer(); ApiStartable s1 = new ApiStartable(); parent.add(s1); parent.startComponents(); SpringComponentContainer child = new SpringComponentContainer(parent); assertThat(child.getParent()).isEqualTo(parent); assertThat(parent.children).containsOnly(child); ApiStartable s2 = new ApiStartable(); child.add(s2); child.startComponents(); parent.stopComponents(); assertThat(s1.stop).isOne(); assertThat(s2.stop).isOne(); } @Test void stop_should_remove_container_from_parent() { SpringComponentContainer parent = new SpringComponentContainer(); SpringComponentContainer child = new SpringComponentContainer(parent); assertThat(parent.children).containsOnly(child); child.stopComponents(); assertThat(parent.children).isEmpty(); } @Test void bean_create_fails_if_class_has_default_constructor_and_other_constructors() { SpringComponentContainer container = new SpringComponentContainer(); container.add(ClassWithMultipleConstructorsIncNoArg.class); container.startComponents(); assertThatThrownBy(() -> container.getComponentByType(ClassWithMultipleConstructorsIncNoArg.class)) .hasRootCauseMessage( "Constructor annotations missing in: class org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainerTests$ClassWithMultipleConstructorsIncNoArg"); } @Test void support_start_stop_callbacks() { JsrLifecycleCallbacks jsr = new JsrLifecycleCallbacks(); ApiStartable api = new ApiStartable(); AutoClose closeable = new AutoClose(); SpringComponentContainer container = new SimpleContainer(jsr, api, closeable) { @Override public void doAfterStart() { // force lazy instantiation getComponentByType(JsrLifecycleCallbacks.class); getComponentByType(ApiStartable.class); getComponentByType(AutoClose.class); } }; container.execute(null); assertThat(closeable.closed).isOne(); assertThat(jsr.postConstruct).isOne(); assertThat(jsr.preDestroy).isOne(); assertThat(api.start).isOne(); assertThat(api.stop).isOne(); } private static class JsrLifecycleCallbacks { private int postConstruct = 0; private int preDestroy = 0; @PostConstruct public void postConstruct() { postConstruct++; } @PreDestroy public void preDestroy() { preDestroy++; } } private static class AutoClose implements AutoCloseable { private int closed = 0; @Override public void close() { closed++; } } private static class ApiStartable implements Startable { private int start = 0; private int stop = 0; @Override public void start() { start++; } @Override public void stop() { stop++; } } private static class ToString { private final String toString; public ToString(String toString) { this.toString = toString; } @Override public String toString() { return toString; } } @Property(key = "k", name = "name") private static class A { } private static class B { } private static class ClassWithMultipleConstructorsIncNoArg { public ClassWithMultipleConstructorsIncNoArg() { } public ClassWithMultipleConstructorsIncNoArg(A a) { } } private static class ExtensionWithMultipleConstructorsAndNoAnnotations { private boolean gotBothArgs = false; public ExtensionWithMultipleConstructorsAndNoAnnotations(A a) { } public ExtensionWithMultipleConstructorsAndNoAnnotations(A a, B b) { gotBothArgs = true; } } private static class ErrorStopClass implements Startable { private boolean stopped = false; @Override public void start() { } @Override public void stop() { stopped = true; throw new IllegalStateException("stop"); } } private static class SimpleContainer extends SpringComponentContainer { public SimpleContainer(Object... objects) { add(objects); } } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/container/StartableBeanPostProcessorTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.container; import org.junit.jupiter.api.Test; import org.sonar.api.Startable; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; class StartableBeanPostProcessorTests { private final StartableBeanPostProcessor underTest = new StartableBeanPostProcessor(); @Test void starts_api_startable() { Startable startable = mock(Startable.class); underTest.postProcessBeforeInitialization(startable, "startable"); verify(startable).start(); verifyNoMoreInteractions(startable); } @Test void stops_api_startable() { Startable startable = mock(Startable.class); underTest.postProcessBeforeDestruction(startable, "startable"); verify(startable).stop(); verifyNoMoreInteractions(startable); } @Test void startable_and_autoCloseable_should_require_destruction() { assertThat(underTest.requiresDestruction(mock(Startable.class))).isTrue(); assertThat(underTest.requiresDestruction(mock(org.sonar.api.Startable.class))).isTrue(); assertThat(underTest.requiresDestruction(mock(Object.class))).isFalse(); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginClassloaderFactoryTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; import org.sonar.api.server.rule.RulesDefinition; import org.sonarsource.sonarlint.plugin.api.module.file.ModuleFileListener; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; class PluginClassloaderFactoryTests { private static final String BASE_PLUGIN_CLASSNAME = "org.sonar.plugins.base.BasePlugin"; private static final String DEPENDENT_PLUGIN_CLASSNAME = "org.sonar.plugins.dependent.DependentPlugin"; private static final String BASE_PLUGIN_KEY = "base"; private static final String DEPENDENT_PLUGIN_KEY = "dependent"; private final PluginClassloaderFactory factory = new PluginClassloaderFactory(); @Test void create_isolated_classloader() { var def = basePluginDef(); var map = factory.create(getClass().getClassLoader(), List.of(def)); assertThat(map).containsOnlyKeys(def); var classLoader = map.get(def); // plugin can access to sonar-plugin-api classes... assertThat(canLoadClass(classLoader, RulesDefinition.class.getCanonicalName())).isTrue(); // ... to sonarlint-plugin-api classes... assertThat(canLoadClass(classLoader, ModuleFileListener.class.getCanonicalName())).isTrue(); // ... and of course to its own classes ! assertThat(canLoadClass(classLoader, BASE_PLUGIN_CLASSNAME)).isTrue(); // plugin can not access to core classes assertThat(canLoadClass(classLoader, PluginClassloaderFactory.class.getCanonicalName())).isFalse(); assertThat(canLoadClass(classLoader, Test.class.getCanonicalName())).isFalse(); assertThat(canLoadClass(classLoader, StringUtils.class.getCanonicalName())).isFalse(); } @Test void classloader_exports_resources_to_other_classloaders() { var baseDef = basePluginDef(); var dependentDef = dependentPluginDef(); var map = factory.create(getClass().getClassLoader(), asList(baseDef, dependentDef)); var baseClassloader = map.get(baseDef); var dependentClassloader = map.get(dependentDef); // base-plugin exports its API package to other plugins assertThat(canLoadClass(dependentClassloader, "org.sonar.plugins.base.api.BaseApi")).isTrue(); assertThat(canLoadClass(dependentClassloader, BASE_PLUGIN_CLASSNAME)).isFalse(); assertThat(canLoadClass(dependentClassloader, DEPENDENT_PLUGIN_CLASSNAME)).isTrue(); // dependent-plugin does not export its classes assertThat(canLoadClass(baseClassloader, DEPENDENT_PLUGIN_CLASSNAME)).isFalse(); assertThat(canLoadClass(baseClassloader, BASE_PLUGIN_CLASSNAME)).isTrue(); } private static PluginClassLoaderDef basePluginDef() { var def = new PluginClassLoaderDef(BASE_PLUGIN_KEY); def.addMainClass(BASE_PLUGIN_KEY, BASE_PLUGIN_CLASSNAME); def.getExportMaskBuilder().include("org/sonar/plugins/base/api/"); def.addFiles(List.of(testPluginJar("base-plugin/target/base-plugin-0.1-SNAPSHOT.jar"))); return def; } private static PluginClassLoaderDef dependentPluginDef() { var def = new PluginClassLoaderDef(DEPENDENT_PLUGIN_KEY); def.addMainClass(DEPENDENT_PLUGIN_KEY, DEPENDENT_PLUGIN_CLASSNAME); def.getExportMaskBuilder().include("org/sonar/plugins/dependent/api/"); def.addFiles(List.of(testPluginJar("dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar"))); return def; } static File testPluginJar(String path) { var file = Paths.get("src/test/projects/" + path); if (!Files.exists(file)) { throw new IllegalArgumentException("Test projects are not built: " + path); } return file.toFile(); } private static boolean canLoadClass(ClassLoader classloader, String classname) { try { classloader.loadClass(classname); return true; } catch (ClassNotFoundException e) { return false; } } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginInfoTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.internal.apachecommons.io.FileUtils; import org.sonar.api.utils.ZipUtils; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginManifest.RequiredPlugin; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class PluginInfoTests { @Test void test_equals() { var java1 = new PluginInfo("java").setVersion(Version.create("1.0")); var java2 = new PluginInfo("java").setVersion(Version.create("2.0")); var javaNoVersion = new PluginInfo("java"); var cobol = new PluginInfo("cobol").setVersion(Version.create("1.0")); assertThat(java1.equals(java1)).isTrue(); assertThat(java1.equals(java2)).isFalse(); assertThat(java1.equals(javaNoVersion)).isFalse(); assertThat(java1.equals(cobol)).isFalse(); assertThat(java1.equals("java:1.0")).isFalse(); assertThat(java1.equals(null)).isFalse(); assertThat(javaNoVersion.equals(javaNoVersion)).isTrue(); assertThat(java1).hasSameHashCodeAs(java1); assertThat(javaNoVersion).hasSameHashCodeAs(javaNoVersion); } @Test void test_compatibility_with_sq_version() { assertThat(withMinSqVersion("1.1").isCompatibleWith("1.1")).isTrue(); assertThat(withMinSqVersion("1.1").isCompatibleWith("1.1.0")).isTrue(); assertThat(withMinSqVersion("1.0").isCompatibleWith("1.0.0")).isTrue(); assertThat(withMinSqVersion("1.0").isCompatibleWith("1.1")).isTrue(); assertThat(withMinSqVersion("1.1.1").isCompatibleWith("1.1.2")).isTrue(); assertThat(withMinSqVersion("2.0").isCompatibleWith("2.1.0")).isTrue(); assertThat(withMinSqVersion("3.2").isCompatibleWith("3.2-RC1")).isTrue(); assertThat(withMinSqVersion("3.2").isCompatibleWith("3.2-RC2")).isTrue(); assertThat(withMinSqVersion("3.2").isCompatibleWith("3.1-RC2")).isFalse(); assertThat(withMinSqVersion("1.1").isCompatibleWith("1.0")).isFalse(); assertThat(withMinSqVersion("2.0.1").isCompatibleWith("2.0.0")).isTrue(); assertThat(withMinSqVersion("2.10").isCompatibleWith("2.1")).isFalse(); assertThat(withMinSqVersion("10.10").isCompatibleWith("2.2")).isFalse(); assertThat(withMinSqVersion("1.1-SNAPSHOT").isCompatibleWith("1.0")).isFalse(); assertThat(withMinSqVersion("1.1-SNAPSHOT").isCompatibleWith("1.1")).isTrue(); assertThat(withMinSqVersion("1.1-SNAPSHOT").isCompatibleWith("1.2")).isTrue(); assertThat(withMinSqVersion("1.0.1-SNAPSHOT").isCompatibleWith("1.0")).isTrue(); assertThat(withMinSqVersion("3.1-RC2").isCompatibleWith("3.2-SNAPSHOT")).isTrue(); assertThat(withMinSqVersion("3.1-RC1").isCompatibleWith("3.2-RC2")).isTrue(); assertThat(withMinSqVersion("3.1-RC1").isCompatibleWith("3.1-RC2")).isTrue(); assertThat(withMinSqVersion(null).isCompatibleWith("0")).isTrue(); assertThat(withMinSqVersion(null).isCompatibleWith("3.1")).isTrue(); assertThat(withMinSqVersion("7.0.0.12345").isCompatibleWith("7.0")).isTrue(); } @Test void create_from_minimal_manifest(@TempDir Path temp) throws Exception { var manifest = mock(SonarPluginManifest.class); when(manifest.getKey()).thenReturn("java"); when(manifest.getVersion()).thenReturn("1.0"); when(manifest.getName()).thenReturn("Java"); when(manifest.getMainClass()).thenReturn("org.foo.FooPlugin"); var jarFile = temp.resolve("myPlugin.jar"); var pluginInfo = PluginInfo.create(jarFile, manifest); assertThat(pluginInfo.getKey()).isEqualTo("java"); assertThat(pluginInfo.getName()).isEqualTo("Java"); assertThat(pluginInfo.getVersion().getName()).isEqualTo("1.0"); assertThat(pluginInfo.getJarFile()).isEqualTo(jarFile.toFile()); assertThat(pluginInfo.getMainClass()).isEqualTo("org.foo.FooPlugin"); assertThat(pluginInfo.getBasePlugin()).isNull(); assertThat(pluginInfo.getMinimalSqVersion()).isNull(); assertThat(pluginInfo.getRequiredPlugins()).isEmpty(); assertThat(pluginInfo.getJreMinVersion()).isNull(); assertThat(pluginInfo.getNodeJsMinVersion()).isNull(); } @Test void create_from_complete_manifest(@TempDir Path temp) throws Exception { var manifest = mock(SonarPluginManifest.class); when(manifest.getKey()).thenReturn("fbcontrib"); when(manifest.getVersion()).thenReturn("2.0"); when(manifest.getName()).thenReturn("Java"); when(manifest.getMainClass()).thenReturn("org.fb.FindbugsPlugin"); when(manifest.getBasePluginKey()).thenReturn("findbugs"); when(manifest.getSonarMinVersion()).thenReturn(Optional.of(Version.create("4.5.1"))); when(manifest.getRequiredPlugins()).thenReturn(List.of(new RequiredPlugin("java", Version.create("2.0")), new RequiredPlugin("pmd", Version.create("1.3")))); when(manifest.getJreMinVersion()).thenReturn(Optional.of(Version.create("11"))); when(manifest.getNodeJsMinVersion()).thenReturn(Optional.of(Version.create("12.18.3"))); var jarFile = temp.resolve("myPlugin.jar"); var pluginInfo = PluginInfo.create(jarFile, manifest); assertThat(pluginInfo.getBasePlugin()).isEqualTo("findbugs"); assertThat(pluginInfo.getMinimalSqVersion().getName()).isEqualTo("4.5.1"); assertThat(pluginInfo.getRequiredPlugins()).extracting("key").containsOnly("java", "pmd"); assertThat(pluginInfo.getJreMinVersion().getName()).isEqualTo("11"); assertThat(pluginInfo.getNodeJsMinVersion().getName()).isEqualTo("12.18.3"); } @Test void create_from_file() throws URISyntaxException { var checkstyleJar = Paths.get(getClass().getResource("/sonar-checkstyle-plugin-2.8.jar").toURI()); var checkstyleInfo = PluginInfo.create(checkstyleJar); assertThat(checkstyleInfo.getName()).isEqualTo("Checkstyle"); assertThat(checkstyleInfo.getMinimalSqVersion()).isEqualTo(Version.create("2.8")); } @Test void test_toString() throws Exception { var pluginInfo = new PluginInfo("java").setVersion(Version.create("1.1")); assertThat(pluginInfo).hasToString("[java / 1.1]"); } @Test void fail_when_jar_is_not_a_plugin(@TempDir Path temp) throws IOException { // this JAR has a manifest but is not a plugin var jarRootDir = Files.createTempDirectory(temp, "myPlugin").toFile(); FileUtils.write(new File(jarRootDir, "META-INF/MANIFEST.MF"), "Build-Jdk: 1.6.0_15", StandardCharsets.UTF_8); var jar = temp.resolve("myPlugin.jar"); ZipUtils.zipDir(jarRootDir, jar.toFile()); var thrown = assertThrows(IllegalStateException.class, () -> PluginInfo.create(jar)); assertThat(thrown).hasMessage("Error while reading plugin manifest from jar: " + jar.toAbsolutePath()); } PluginInfo withMinSqVersion(@Nullable String version) { var pluginInfo = new PluginInfo("foo"); if (version != null) { pluginInfo.setMinimalSqVersion(Version.create(version)); } return pluginInfo; } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/loading/PluginInstancesLoaderTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Map; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.Plugin; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.groups.Tuple.tuple; class PluginInstancesLoaderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); PluginInstancesLoader loader = new PluginInstancesLoader(new PluginClassloaderFactory()); @AfterEach void closeLoader() throws IOException { loader.close(); } @Test void instantiate_plugin_entry_point() { var def = new PluginClassLoaderDef("fake"); def.addMainClass("fake", FakePlugin.class.getName()); var instances = loader.instantiatePluginClasses(Map.of(def, getClass().getClassLoader())); assertThat(instances).containsOnlyKeys("fake"); assertThat(instances.get("fake")).isInstanceOf(FakePlugin.class); } @Test void plugin_entry_point_must_be_no_arg_public() { var def = new PluginClassLoaderDef("fake"); def.addMainClass("fake", IncorrectPlugin.class.getName()); loader.instantiatePluginClasses(Map.of(def, getClass().getClassLoader())); assertThat(logTester.logs(LogOutput.Level.ERROR)) .contains("Fail to instantiate class [org.sonarsource.sonarlint.core.plugin.commons.loading.PluginInstancesLoaderTests$IncorrectPlugin] of plugin [fake]"); } @Test void define_classloader(@TempDir Path tmp) throws IOException { var jarFile = tmp.resolve("fakePlugin.jar").toFile(); Files.createFile(jarFile.toPath()); var info = new PluginInfo("foo") .setJarFile(jarFile) .setMainClass("org.foo.FooPlugin") .setMinimalSqVersion(Version.create("5.2")); var defs = loader.defineClassloaders(Map.of("foo", info)); assertThat(defs).hasSize(1); var def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.getFiles()).containsExactly(jarFile); assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin")); // TODO test mask - require change in sonar-classloader } @Test void extract_dependencies() { var jarFile = getFile("sonar-checkstyle-plugin-2.8.jar"); var info = new PluginInfo("checkstyle") .setJarFile(jarFile) .setMainClass("org.foo.FooPlugin") .setDependencies(List.of("META-INF/lib/commons-cli-1.0.jar", "META-INF/lib/checkstyle-5.1.jar", "META-INF/lib/antlr-2.7.6.jar")); var defs = loader.defineClassloaders(Map.of("checkstyle", info)); assertThat(defs).hasSize(1); var def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("checkstyle"); assertThat(def.getFiles()).hasSize(4); assertThat(def.getFiles()).extracting(File::getName, f -> { try { return DigestUtils.md5Hex(Files.readAllBytes(f.toPath())); } catch (IOException e) { return e.getMessage(); } }).containsExactlyInAnyOrder( tuple("sonar-checkstyle-plugin-2.8.jar", "e7e5e17e5e297ac88d08122c56d72eb7"), tuple("commons-cli-1.0.jar", "d784fa8b6d98d27699781bd9a7cf19f0"), tuple("checkstyle-5.1.jar", "d784fa8b6d98d27699781bd9a7cf19f0"), tuple("antlr-2.7.6.jar", "d784fa8b6d98d27699781bd9a7cf19f0")); } /** * A plugin (the "base" plugin) can be extended by other plugins. In this case they share the same classloader. */ @Test void test_plugins_sharing_the_same_classloader(@TempDir Path tmp) throws IOException { var baseJarFile = tmp.resolve("fakeBasePlugin.jar").toFile(); baseJarFile.createNewFile(); var extensionJar1 = tmp.resolve("fakePlugin1.jar").toFile(); extensionJar1.createNewFile(); var extensionJar2 = tmp.resolve("fakePlugin2.jar").toFile(); extensionJar2.createNewFile(); var base = new PluginInfo("foo") .setJarFile(baseJarFile) .setMainClass("org.foo.FooPlugin"); var extension1 = new PluginInfo("fooExtension1") .setJarFile(extensionJar1) .setMainClass("org.foo.Extension1Plugin") .setBasePlugin("foo"); var extension2 = new PluginInfo("fooExtension2") .setJarFile(extensionJar2) .setMainClass("org.foo.Extension2Plugin") .setBasePlugin("foo"); var defs = loader.defineClassloaders(Map.of( base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2)); assertThat(defs).hasSize(1); var def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2); assertThat(def.getMainClassesByPluginKey()).containsOnly( entry("foo", "org.foo.FooPlugin"), entry("fooExtension1", "org.foo.Extension1Plugin"), entry("fooExtension2", "org.foo.Extension2Plugin")); // TODO test mask - require change in sonar-classloader } // SLCORE-222 @Test void skip_plugins_when_base_plugin_missing(@TempDir Path tmp) throws IOException { var extensionJar1 = tmp.resolve("fakePlugin1.jar").toFile(); extensionJar1.createNewFile(); var extensionJar2 = tmp.resolve("fakePlugin2.jar").toFile(); extensionJar2.createNewFile(); var extension1 = new PluginInfo("fooExtension1") .setJarFile(extensionJar1) .setMainClass("org.foo.Extension1Plugin"); var extension2 = new PluginInfo("fooExtension2") .setJarFile(extensionJar2) .setMainClass("org.foo.Extension2Plugin") .setBasePlugin("foo"); var defs = loader.defineClassloaders(Map.of( extension1.getKey(), extension1, extension2.getKey(), extension2)); assertThat(defs).hasSize(1); var def = defs.iterator().next(); assertThat(def.getFiles()).containsOnly(extensionJar1); assertThat(def.getMainClassesByPluginKey()).containsOnly( entry("fooExtension1", "org.foo.Extension1Plugin")); } // SLCORE-557 @Test void should_be_able_to_delete_jar_after_unload() throws IOException { var jarFile = PluginClassloaderFactoryTests.testPluginJar("classloader-leak-plugin/target/classloader-leak-plugin-0.1-SNAPSHOT.jar"); var tmpCopy = Files.createTempFile("leak-plugin", ".jar"); Files.copy(jarFile.toPath(), tmpCopy, StandardCopyOption.REPLACE_EXISTING); var info = new PluginInfo("leak") .setJarFile(tmpCopy.toFile()) .setMainClass("org.sonar.plugins.leak.LeakPlugin"); var instances = loader.instantiatePluginClasses(List.of(info)); var instance = instances.get("leak"); // The code in the plugin will leak a file handle, see https://bugs.java.com/bugdatabase/view_bug?bug_id=JDK-8315993 instance.define(null); loader.close(); Files.delete(tmpCopy); } public static class FakePlugin implements Plugin { @Override public void define(Context context) { // no extensions } } /** * No public empty-param constructor */ public static class IncorrectPlugin implements Plugin { public IncorrectPlugin(String s) { } @Override public void define(Context context) { // no extensions } } private File getFile(String filename) { return FileUtils.toFile(getClass().getResource("/" + filename)); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/loading/SonarPluginManifestTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginManifest.RequiredPlugin; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class SonarPluginManifestTests { @Test void test_RequiredPlugin() throws Exception { var plugin = SonarPluginManifest.RequiredPlugin.parse("java:1.1"); assertThat(plugin.getKey()).isEqualTo("java"); assertThat(plugin.getMinimalVersion().getName()).isEqualTo("1.1"); assertThrows(IllegalArgumentException.class, () -> SonarPluginManifest.RequiredPlugin.parse("java")); } @Test void test() { var fake = Paths.get("fake.jar"); assertThrows(RuntimeException.class, () -> SonarPluginManifest.fromJar(fake)); } @Test void should_create_manifest_from_jar() throws URISyntaxException, IOException { var checkstyleJar = Paths.get(getClass().getResource("/sonar-checkstyle-plugin-2.8.jar").toURI()); var manifest = SonarPluginManifest.fromJar(checkstyleJar); assertThat(manifest.getKey()).isEqualTo("checkstyle"); assertThat(manifest.getName()).isEqualTo("Checkstyle"); assertThat(manifest.getRequiredPlugins()).isEmpty(); assertThat(manifest.getMainClass()).isEqualTo("org.sonar.plugins.checkstyle.CheckstylePlugin"); assertThat(manifest.getVersion().length()).isGreaterThan(1); assertThat(manifest.getJreMinVersion()).isEmpty(); assertThat(manifest.getNodeJsMinVersion()).isEmpty(); } @Test void should_add_requires_plugins() throws URISyntaxException, IOException { var jar = getClass().getResource("/SonarPluginManifestTests/plugin-with-require-plugins.jar"); var manifest = SonarPluginManifest.fromJar(Paths.get(jar.toURI())); assertThat(manifest.getRequiredPlugins()) .usingRecursiveFieldByFieldElementComparator() .containsExactlyInAnyOrder(new RequiredPlugin("scm", Version.create("1.0")), new RequiredPlugin("fake", Version.create("1.1"))); } @Test void should_parse_jre_min_version() throws URISyntaxException, IOException { var jar = getClass().getResource("/SonarPluginManifestTests/plugin-with-jre-min.jar"); var manifest = SonarPluginManifest.fromJar(Paths.get(jar.toURI())); assertThat(manifest.getJreMinVersion()).contains(Version.create("11")); } @Test void should_default_jre_min_version_to_null() throws URISyntaxException, IOException { var jar = getClass().getResource("/SonarPluginManifestTests/plugin-without-jre-min.jar"); var manifest = SonarPluginManifest.fromJar(Paths.get(jar.toURI())); assertThat(manifest.getJreMinVersion()).isEmpty(); } @Test void should_parse_nodejs_min_version() throws URISyntaxException, IOException { var jar = getClass().getResource("/SonarPluginManifestTests/plugin-with-nodejs-min.jar"); var manifest = SonarPluginManifest.fromJar(Paths.get(jar.toURI())); assertThat(manifest.getNodeJsMinVersion()).contains(Version.create("12.18.3")); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/loading/SonarPluginRequirementsCheckerTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.loading; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.utils.ZipUtils; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.plugins.SonarPlugin; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason; import org.sonarsource.sonarlint.core.plugin.commons.api.SkipReason.UnsatisfiedRuntimeRequirement.RuntimeRequirement; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.plugin.commons.loading.SonarPluginRequirementsChecker.isCompatibleWith; class SonarPluginRequirementsCheckerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final Set NONE = Set.of(); private static final String V1_0 = "1.0"; private static final String FAKE_PLUGIN_KEY = "pluginkey"; private static final org.sonar.api.utils.Version FAKE_PLUGIN_API_VERSION = org.sonar.api.utils.Version.parse("8.1.2"); private SonarPluginRequirementsChecker underTest; @BeforeEach void prepare() { underTest = new SonarPluginRequirementsChecker(FAKE_PLUGIN_API_VERSION); } @Test void load_no_plugins() { var loadedPlugins = underTest.checkRequirements(Set.of(), NONE, null, null, false); assertThat(loadedPlugins).isEmpty(); } @Test void load_plugin_fail_if_missing_jar() { Set jars = Set.of(Paths.get("doesntexists.jar")); var checkRequirements = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(checkRequirements).isEmpty(); assertThat(logTester.logs(Level.ERROR)).contains("Unable to load plugin doesntexists.jar"); } @Test void load_plugin_skip_corrupted_jar(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "sonarjs.jar"); Set jars = Set.of(fakePlugin); var checkRequirements = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(checkRequirements).isEmpty(); assertThat(logTester.logs(Level.ERROR)).contains("Unable to load plugin " + fakePlugin); } @Test void load_plugin_skip_unsupported_plugins_api_version(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "sonarjs.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withSqApiVersion("99.9"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, SkipReason.IncompatiblePluginApi.INSTANCE)); assertThat(logsWithoutStartStop()) .contains("Plugin 'pluginkey' requires plugin API 99.9 while SonarLint supports only up to " + FAKE_PLUGIN_API_VERSION + ". Skip loading it."); } @Test void load_plugin_skip_not_enabled_languages(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "sonarphp.jar", path -> createPluginManifest(path, SonarPlugin.PHP.getKey(), V1_0)); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.TS), null, null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("php", true, new SkipReason.LanguagesNotEnabled(new HashSet<>(List.of(SonarLanguage.PHP))))); assertThat(logsWithoutStartStop()).contains("Plugin 'php' is excluded because language 'PHP' is not enabled. Skip loading it."); } @Test void load_plugin_skip_not_enabled_languages_multiple(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "sonarjs.jar", path -> createPluginManifest(path, SonarPlugin.C_FAMILY.getKey(), V1_0)); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.JS), null, null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("cpp", true, new SkipReason.LanguagesNotEnabled(new HashSet<>(asList(SonarLanguage.C, SonarLanguage.CPP, SonarLanguage.OBJC))))); assertThat(logsWithoutStartStop()).contains("Plugin 'cpp' is excluded because none of languages 'C,CPP,OBJC' are enabled. Skip loading it."); } @Test void load_plugin_load_even_if_only_one_language_enabled(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "sonarjs.jar", path -> createPluginManifest(path, SonarPlugin.C_FAMILY.getKey(), V1_0)); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.CPP), null, null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("cpp", false, null)); } @Test void load_plugin_skip_plugins_having_missing_base_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withBasePlugin(SonarPlugin.JS.getKey()))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedDependency("javascript"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' dependency on 'javascript' is unsatisfied. Skip loading it."); } @Test void load_plugin_skip_plugins_having_skipped_base_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withBasePlugin(SonarPlugin.JS.getKey()))); var fakeBasePlugin = fakePlugin(storage, "base.jar", path -> createPluginManifest(path, SonarPlugin.JS.getKey(), V1_0)); Set jars = Set.of(fakePlugin, fakeBasePlugin); // Ensure base plugin is skipped because JS language is not enabled var enabledLanguages = Set.of(SonarLanguage.C); var loadedPlugins = underTest.checkRequirements(jars, enabledLanguages, null, null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly( tuple("pluginkey", true, new SkipReason.UnsatisfiedDependency("javascript")), tuple("javascript", true, new SkipReason.LanguagesNotEnabled(List.of(SonarLanguage.CSS, SonarLanguage.JS, SonarLanguage.TS, SonarLanguage.YAML, SonarLanguage.JSON)))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' dependency on 'javascript' is unsatisfied. Skip loading it."); } @Test void load_plugin_having_base_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withBasePlugin(SonarPlugin.JS.getKey()))); var fakeBasePlugin = fakePlugin(storage, "base.jar", path -> createPluginManifest(path, SonarPlugin.JS.getKey(), V1_0)); Set jars = Set.of(fakePlugin, fakeBasePlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.JS), null, null, false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly( tuple("pluginkey", false, null), tuple("javascript", false, null)); } @Test void load_plugin_skip_plugins_having_missing_required_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withRequiredPlugins("required2:1.0"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedDependency("required2"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' dependency on 'required2' is unsatisfied. Skip loading it."); } @Test void load_plugin_skip_plugins_having_skipped_required_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withRequiredPlugins("required2:1.0"))); var fakeDepPlugin = fakePlugin(storage, "dep.jar", path -> createPluginManifest(path, "required2", V1_0, withNodejsMinVersion("99.9.9"))); Set jars = Set.of(fakePlugin, fakeDepPlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.of(Version.create("0.1.2")), false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedDependency("required2")), tuple("required2", true, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, "0.1.2", "99.9.9"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' dependency on 'required2' is unsatisfied. Skip loading it."); } @Test void load_plugin(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withSqApiVersion("7.9"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, null, false); assertThat(loadedPlugins.values()).as(logsWithoutStartStop().collect(Collectors.joining("\n"))) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped) .containsOnly(tuple(FAKE_PLUGIN_KEY, false)); } @Test void load_plugin_skip_plugins_having_unsatisfied_jre(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withJreMinVersion("11"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, Version.create("1.8"), null, false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.JRE, "1.8", "11"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' requires JRE 11 while current is 1.8. Skip loading it."); } @Test void load_plugin_having_satisfied_nodejs(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withNodejsMinVersion("10.1.2"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.of(Version.create("10.1.3")), false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped) .containsOnly(tuple("pluginkey", false)); } @Test void load_plugin_having_satisfied_nodejs_nightly(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withNodejsMinVersion("15.0.0"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.of(Version.create("15.0.0-nightly20200921039c274dde")), false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped) .containsOnly(tuple("pluginkey", false)); } @Test void load_plugin_skip_plugins_having_unsatisfied_nodejs_version(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withNodejsMinVersion("10.1.2"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.of(Version.create("10.1.1")), false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, "10.1.1", "10.1.2"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' requires Node.js 10.1.2 while current is 10.1.1. Skip loading it."); } @Test void load_plugin_skip_plugins_having_unsatisfied_nodejs(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withNodejsMinVersion("10.1.2"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.empty(), false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("pluginkey", true, new SkipReason.UnsatisfiedRuntimeRequirement(RuntimeRequirement.NODEJS, null, "10.1.2"))); assertThat(logsWithoutStartStop()).contains("Plugin 'pluginkey' requires Node.js 10.1.2. Skip loading it."); } @Test void load_plugin_having_satisfied_jre(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, FAKE_PLUGIN_KEY, V1_0, withJreMinVersion("1.7"))); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, Version.create("1.8"), Optional.empty(), false); assertThat(loadedPlugins.values()) .extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped) .containsOnly(tuple("pluginkey", false)); } @Test void load_plugin_skip_plugins_having_unsatisfied_python_frontend_dbd(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, "dbdpythonfrontend", "1.15")); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.empty(), false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("dbdpythonfrontend", true, SkipReason.UnsupportedFeature.INSTANCE)); assertThat(logsWithoutStartStop()).contains("DBD feature disabled. Skip loading plugin 'dbdpythonfrontend'."); } @Test void load_plugin_skip_plugins_having_unsatisfied_python_dbd(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, "dbd", "1.15")); Set jars = Set.of(fakePlugin); var loadedPlugins = underTest.checkRequirements(jars, NONE, null, Optional.empty(), true); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly(tuple("dbd", true, new SkipReason.UnsatisfiedDependency(SonarPlugin.PYTHON.getKey()))); assertThat(logsWithoutStartStop()).contains("Plugin 'dbd' dependency on 'python' is unsatisfied. Skip loading it."); } @Test void load_plugin_having_satisfied_python_frontend_dbd(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, "dbdpythonfrontend", "1.15")); var fakePythonPlugin = fakePlugin(storage, "python.jar", path -> createPluginManifest(path, SonarPlugin.PYTHON.getKey(), "3.25")); Set jars = Set.of(fakePlugin, fakePythonPlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.PYTHON), null, Optional.empty(), true); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly( tuple("python", false, null), tuple("dbdpythonfrontend", false, null) ); } @Test void load_plugin_having_satisfied_python_dbd(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, "dbd", "1.15")); var fakePythonPlugin = fakePlugin(storage, "python.jar", path -> createPluginManifest(path, SonarPlugin.PYTHON.getKey(), "3.25")); Set jars = Set.of(fakePlugin, fakePythonPlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.PYTHON), null, Optional.empty(), true); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly( tuple("python", false, null), tuple("dbd", false, null) ); } @Test void load_plugin_having_satisfied_python_dbd_but_no_feature_flag(@TempDir Path storage) throws IOException { var fakePlugin = fakePlugin(storage, "fake.jar", path -> createPluginManifest(path, "dbd", "1.15")); var fakePythonPlugin = fakePlugin(storage, "python.jar", path -> createPluginManifest(path, SonarPlugin.PYTHON.getKey(), "3.25")); Set jars = Set.of(fakePlugin, fakePythonPlugin); var loadedPlugins = underTest.checkRequirements(jars, Set.of(SonarLanguage.PYTHON), null, Optional.empty(), false); assertThat(loadedPlugins.values()).extracting(r -> r.getPlugin().getKey(), PluginRequirementsCheckResult::isSkipped, p -> p.getSkipReason().orElse(null)) .containsOnly( tuple("python", false, null), tuple("dbd", true, SkipReason.UnsupportedFeature.INSTANCE) ); assertThat(logsWithoutStartStop()).contains("DBD feature disabled. Skip loading plugin 'dbd'."); } @Test void test_isCompatibleWith() { assertThat(isCompatibleWith(withMinSqVersion("1.1"), Version.create("1.1"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.1"), Version.create("1.1.0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.0"), Version.create("1.0.0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.0"), Version.create("1.1"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.1.1"), Version.create("1.1.2"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("2.0"), Version.create("2.1.0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.2"), Version.create("3.2-RC1"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.2"), Version.create("3.2-RC2"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.2"), Version.create("3.1-RC2"))).isFalse(); assertThat(isCompatibleWith(withMinSqVersion("1.1"), Version.create("1.0"))).isFalse(); assertThat(isCompatibleWith(withMinSqVersion("2.0.1"), Version.create("2.0.0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("2.10"), Version.create("2.1"))).isFalse(); assertThat(isCompatibleWith(withMinSqVersion("10.10"), Version.create("2.2"))).isFalse(); assertThat(isCompatibleWith(withMinSqVersion("1.1-SNAPSHOT"), Version.create("1.0"))).isFalse(); assertThat(isCompatibleWith(withMinSqVersion("1.1-SNAPSHOT"), Version.create("1.1"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.1-SNAPSHOT"), Version.create("1.2"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("1.0.1-SNAPSHOT"), Version.create("1.0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.1-RC2"), Version.create("3.2-SNAPSHOT"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.1-RC1"), Version.create("3.2-RC2"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("3.1-RC1"), Version.create("3.1-RC2"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion(null), Version.create("0"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion(null), Version.create("3.1"))).isTrue(); assertThat(isCompatibleWith(withMinSqVersion("7.0.0.12345"), Version.create("7.0"))).isTrue(); } PluginInfo withMinSqVersion(@Nullable String version) { var plugin = mock(PluginInfo.class); when(plugin.getKey()).thenReturn("foo"); when(plugin.getMinimalSqVersion()).thenReturn(Optional.ofNullable(version).map(Version::create).orElse(null)); return plugin; } private void createPluginManifest(Path path, String pluginKey, String version, Consumer... manifestAttributesPopulators) { var manifestPath = path.resolve(JarFile.MANIFEST_NAME); try { Files.createDirectories(manifestPath.getParent()); var manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue(SonarPluginManifest.KEY_ATTRIBUTE, pluginKey); manifest.getMainAttributes().putValue(SonarPluginManifest.VERSION_ATTRIBUTE, version); Stream.of(manifestAttributesPopulators).forEach(p -> p.accept(manifest.getMainAttributes())); try (var fos = Files.newOutputStream(manifestPath, StandardOpenOption.CREATE_NEW)) { manifest.write(fos); } } catch (IOException e) { throw new IllegalStateException(e); } } private Consumer withSqApiVersion(String sqApiVersion) { return a -> a.putValue(SonarPluginManifest.SONAR_VERSION_ATTRIBUTE, sqApiVersion); } private Consumer withJreMinVersion(String jreMinVersion) { return a -> a.putValue(SonarPluginManifest.JRE_MIN_VERSION, jreMinVersion); } private Consumer withNodejsMinVersion(String nodeMinVersion) { return a -> a.putValue(SonarPluginManifest.NODEJS_MIN_VERSION, nodeMinVersion); } private Consumer withBasePlugin(String basePlugin) { return a -> a.putValue(SonarPluginManifest.BASE_PLUGIN, basePlugin); } private Consumer withRequiredPlugins(String... requirePlugins) { return a -> a.putValue(SonarPluginManifest.REQUIRE_PLUGINS_ATTRIBUTE, Stream.of(requirePlugins).collect(joining(","))); } private Path fakePlugin(Path storage, String filename, Consumer... populators) throws IOException { var pluginJar = storage.resolve(filename); var pluginTmpDir = Files.createTempDirectory(storage, "plugin"); Stream.of(populators).forEach(p -> p.accept(pluginTmpDir)); ZipUtils.zipDir(pluginTmpDir.toFile(), pluginJar.toFile()); return pluginJar; } private Stream logsWithoutStartStop() { return logTester.logs().stream() .filter(s -> !s.equals("Load plugins")) .filter(s -> !s.matches("Load plugins \\(done\\) \\| time=(.*)ms")); } } ================================================ FILE: backend/plugin-commons/src/test/java/org/sonarsource/sonarlint/core/plugin/commons/sonarapi/MapSettingsTests.java ================================================ /* * SonarLint Core - Plugin Commons * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.plugin.commons.sonarapi; import java.util.Map; import java.util.Random; import java.util.stream.IntStream; import org.assertj.core.api.Assertions; import org.assertj.core.data.Offset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.Properties; import org.sonar.api.Property; import org.sonar.api.PropertyType; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.utils.System2; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.sonarsource.sonarlint.core.plugin.commons.Utils.randomAlphanumeric; class MapSettingsTests { private PropertyDefinitions definitions; @Properties({ @Property(key = "hello", name = "Hello", defaultValue = "world"), @Property(key = "date", name = "Date", defaultValue = "2010-05-18"), @Property(key = "datetime", name = "DateTime", defaultValue = "2010-05-18T15:50:45+0100"), @Property(key = "boolean", name = "Boolean", defaultValue = "true"), @Property(key = "falseboolean", name = "False Boolean", defaultValue = "false"), @Property(key = "integer", name = "Integer", defaultValue = "12345"), @Property(key = "array", name = "Array", defaultValue = "one,two,three"), @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true), @Property(key = "sonar.jira", name = "Jira Server", type = PropertyType.PROPERTY_SET), @Property(key = "newKey", name = "New key", deprecatedKey = "oldKey"), @Property(key = "newKeyWithDefaultValue", name = "New key with default value", deprecatedKey = "oldKeyWithDefaultValue", defaultValue = "default_value"), @Property(key = "new_multi_values", name = "New multi values", defaultValue = "1,2,3", multiValues = true, deprecatedKey = "old_multi_values") }) private static class Init { } @BeforeEach void init_definitions() { definitions = new PropertyDefinitions(System2.INSTANCE); definitions.addComponent(Init.class); } @Test void set_accepts_empty_value_and_trims_it() { var random = new Random(); var key = randomAlphanumeric(3); var underTest = new MapSettings(Map.of(key, blank(random))); assertThat(underTest.getString(key)).isEmpty(); } @Test void default_values_should_be_loaded_from_definitions() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getDefaultValue("hello")).isEqualTo("world"); } @Test void set_property_string_array_trims_key() { var key = randomAlphanumeric(3); var random = new Random(); var blankBefore = blank(random); var blankAfter = blank(random); var underTest = new MapSettings(new PropertyDefinitions(System2.INSTANCE, singletonList(PropertyDefinition.builder(key).multiValues(true).build())), Map.of(blankBefore + key + blankAfter, "1,2")); assertThat(underTest.hasKey(key)).isTrue(); } private static String blank(Random random) { var b = new StringBuilder(); IntStream.range(0, random.nextInt(3)).mapToObj(s -> " ").forEach(b::append); return b.toString(); } @Test void setProperty_methods_trims_value() { var random = new Random(); var blankBefore = blank(random); var blankAfter = blank(random); var key = randomAlphanumeric(3); var value = randomAlphanumeric(3); var underTest = new MapSettings(Map.of(key, blankBefore + value + blankAfter)); assertThat(underTest.getString(key)).isEqualTo(value); } @Test void set_property_int() { var settings = new MapSettings(Map.of("foo", "123")); assertThat(settings.getInt("foo")).isEqualTo(123); assertThat(settings.getString("foo")).isEqualTo("123"); assertThat(settings.getBoolean("foo")).isFalse(); } @Test void default_number_values_are_zero() { var settings = new MapSettings(Map.of()); assertThat(settings.getInt("foo")).isZero(); assertThat(settings.getLong("foo")).isZero(); } @Test void getInt_value_must_be_valid() { var settings = new MapSettings(Map.of("foo", "not a number")); assertThrows(NumberFormatException.class, () -> settings.getInt("foo")); } @Test void all_values_should_be_trimmed_set_property() { var settings = new MapSettings(Map.of("foo", " FOO ")); assertThat(settings.getString("foo")).isEqualTo("FOO"); } @Test void test_get_default_value() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getDefaultValue("unknown")).isNull(); } @Test void test_get_string() { var settings = new MapSettings(definitions, Map.of("hello", "Russia")); assertThat(settings.getString("hello")).isEqualTo("Russia"); } @Test void test_get_date() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getDate("unknown")).isNull(); assertThat(settings.getDate("date").getDate()).isEqualTo(18); assertThat(settings.getDate("date").getMonth()).isEqualTo(4); } @Test void test_get_date_not_found() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getDate("unknown")).isNull(); } @Test void test_get_datetime() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getDateTime("unknown")).isNull(); assertThat(settings.getDateTime("datetime").getDate()).isEqualTo(18); assertThat(settings.getDateTime("datetime").getMonth()).isEqualTo(4); assertThat(settings.getDateTime("datetime").getMinutes()).isEqualTo(50); } @Test void test_get_double() { var settings = new MapSettings(Map.of("from_string", "3.14159")); assertThat(settings.getDouble("from_string")).isEqualTo(3.14159, Offset.offset(0.00001)); assertThat(settings.getDouble("unknown")).isNull(); } @Test void test_get_float() { var settings = new MapSettings(Map.of("from_string", "3.14159")); assertThat(settings.getDouble("from_string")).isEqualTo(3.14159f, Offset.offset(0.00001)); assertThat(settings.getDouble("unknown")).isNull(); } @Test void test_get_bad_float() { var settings = new MapSettings(Map.of("foo", "bar")); var thrown = assertThrows(IllegalStateException.class, () -> settings.getFloat("foo")); assertThat(thrown).hasMessage("The property 'foo' is not a float value"); } @Test void test_get_bad_double() { var settings = new MapSettings(Map.of("foo", "bar")); var thrown = assertThrows(IllegalStateException.class, () -> settings.getDouble("foo")); assertThat(thrown).hasMessage("The property 'foo' is not a double value"); } @Test void getStringArray() { var settings = new MapSettings(definitions, Map.of()); var array = settings.getStringArray("array"); assertThat(array).isEqualTo(new String[] {"one", "two", "three"}); } @Test void getStringArray_no_value() { var settings = new MapSettings(Map.of()); var array = settings.getStringArray("array"); assertThat(array).isEmpty(); } @Test void shouldTrimArray() { var settings = new MapSettings(Map.of("foo", " one, two, three ")); var array = settings.getStringArray("foo"); assertThat(array).isEqualTo(new String[] {"one", "two", "three"}); } @Test void shouldKeepEmptyValuesWhenSplitting() { var settings = new MapSettings(Map.of("foo", " one, , two")); var array = settings.getStringArray("foo"); assertThat(array).isEqualTo(new String[] {"one", "", "two"}); } @Test void testDefaultValueOfGetString() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getString("hello")).isEqualTo("world"); } @Test void set_property_boolean() { var settings = new MapSettings(Map.of("foo", "true", "bar", "false")); assertThat(settings.getBoolean("foo")).isTrue(); assertThat(settings.getBoolean("bar")).isFalse(); assertThat(settings.getString("foo")).isEqualTo("true"); assertThat(settings.getString("bar")).isEqualTo("false"); } @Test void ignore_case_of_boolean_values() { var settings = new MapSettings(Map.of("foo", "true", "bar", "TRUE", // labels in UI "baz", "True")); assertThat(settings.getBoolean("foo")).isTrue(); assertThat(settings.getBoolean("bar")).isTrue(); assertThat(settings.getBoolean("baz")).isTrue(); } @Test void get_boolean() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getBoolean("boolean")).isTrue(); assertThat(settings.getBoolean("falseboolean")).isFalse(); assertThat(settings.getBoolean("unknown")).isFalse(); assertThat(settings.getBoolean("hello")).isFalse(); } @Test void shouldCreateByIntrospectingComponent() { var settings = new MapSettings(Map.of()); settings.getDefinitions().addComponent(MyComponent.class); // property definition has been loaded, ie for default value assertThat(settings.getDefaultValue("foo")).isEqualTo("bar"); } @Property(key = "foo", name = "Foo", defaultValue = "bar") public static class MyComponent { } @Test void getStringLines_no_value() { Assertions.assertThat(new MapSettings(Map.of()).getStringLines("foo")).isEmpty(); } @Test void getStringLines_single_line() { var settings = new MapSettings(Map.of("foo", "the line")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"the line"}); } @Test void getStringLines_linux() { var settings = new MapSettings(Map.of("foo", "one\ntwo")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); settings = new MapSettings(Map.of("foo", "one\ntwo\n")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); } @Test void getStringLines_windows() { var settings = new MapSettings(Map.of("foo", "one\r\ntwo")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); settings = new MapSettings(Map.of("foo", "one\r\ntwo\r\n")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two"}); } @Test void getStringLines_mix() { var settings = new MapSettings(Map.of("foo", "one\r\ntwo\nthree")); assertThat(settings.getStringLines("foo")).isEqualTo(new String[] {"one", "two", "three"}); } @Test void getKeysStartingWith() { var settings = new MapSettings(Map.of("sonar.jdbc.url", "foo", "sonar.jdbc.username", "bar", "sonar.security", "admin")); assertThat(settings.getKeysStartingWith("sonar")).containsOnly("sonar.jdbc.url", "sonar.jdbc.username", "sonar.security"); assertThat(settings.getKeysStartingWith("sonar.jdbc")).containsOnly("sonar.jdbc.url", "sonar.jdbc.username"); assertThat(settings.getKeysStartingWith("other")).isEmpty(); } @Test void should_fallback_deprecated_key_to_default_value_of_new_key() { var settings = new MapSettings(definitions, Map.of()); assertThat(settings.getString("newKeyWithDefaultValue")).isEqualTo("default_value"); assertThat(settings.getString("oldKeyWithDefaultValue")).isEqualTo("default_value"); } @Test void should_fallback_deprecated_key_to_new_key() { var settings = new MapSettings(definitions, Map.of("newKey", "value of newKey")); assertThat(settings.getString("newKey")).isEqualTo("value of newKey"); assertThat(settings.getString("oldKey")).isEqualTo("value of newKey"); } @Test void should_load_value_of_deprecated_key() { // it's used for example when deprecated settings are set through command-line var settings = new MapSettings(definitions, Map.of("oldKey", "value of oldKey")); assertThat(settings.getString("newKey")).isEqualTo("value of oldKey"); assertThat(settings.getString("oldKey")).isEqualTo("value of oldKey"); } @Test void should_load_values_of_deprecated_key() { var settings = new MapSettings(definitions, Map.of("oldKey", "a,b")); assertThat(settings.getStringArray("newKey")).containsOnly("a", "b"); assertThat(settings.getStringArray("oldKey")).containsOnly("a", "b"); } @Test void should_support_deprecated_props_with_multi_values() { var settings = new MapSettings(definitions, Map.of("new_multi_values", " A , B ")); assertThat(settings.getStringArray("new_multi_values")).isEqualTo(new String[] {"A", "B"}); assertThat(settings.getStringArray("old_multi_values")).isEqualTo(new String[] {"A", "B"}); } @Test void testParsingMultiValues() { assertThat(getStringArray("")).isEmpty(); assertThat(getStringArray(",")).isEmpty(); assertThat(getStringArray(",,")).isEmpty(); assertThat(getStringArray("a")).containsExactly("a"); assertThat(getStringArray("a b")).containsExactly("a b"); assertThat(getStringArray("a , b")).containsExactly("a", "b"); assertThat(getStringArray("\"a \",\" b\"")).containsExactly("a ", " b"); assertThat(getStringArray("\"a,b\",c")).containsExactly("a,b", "c"); assertThat(getStringArray("\"a\nb\",c")).containsExactly("a\nb", "c"); assertThat(getStringArray("\"a\",\n b\n")).containsExactly("a", "b"); assertThat(getStringArray("a\n,b\n")).containsExactly("a", "b"); assertThat(getStringArray("a\n,b\n,\"\"")).containsExactly("a", "b", ""); assertThat(getStringArray("a\n, \" \" ,b\n")).containsExactly("a", " ", "b"); assertThat(getStringArray(" \" , ,, \", a\n,b\n")).containsExactly(" , ,, ", "a", "b"); assertThat(getStringArray("a\n,,b\n")).containsExactly("a", "b"); assertThat(getStringArray("a,\n\nb,c")).containsExactly("a", "b", "c"); assertThat(getStringArray("a,b\n\nc,d")).containsExactly("a", "b\nc", "d"); assertThat(getStringArray("a,\"\",b")).containsExactly("a", "", "b"); var thrown = assertThrows(IllegalStateException.class, () -> getStringArray("\"a ,b")); assertThat(thrown).hasMessage("Property: 'multi_values' doesn't contain a valid CSV value: '\"a ,b'"); } private String[] getStringArray(String value) { var settings = new MapSettings(definitions, Map.of("multi_values", value)); return settings.getStringArray("multi_values"); } } ================================================ FILE: backend/plugin-commons/src/test/projects/.gitignore ================================================ # see README.txt !*/target/ */target/classes/ */target/maven-archiver/ */target/maven-status/ */target/test-*/ ================================================ FILE: backend/plugin-commons/src/test/projects/README.txt ================================================ This directory provides the fake plugins used by tests. These tests are rarely changed, so generated artifacts are stored in Git repository (see .gitignore). It avoids from adding unnecessary modules to build. ================================================ FILE: backend/plugin-commons/src/test/projects/base-plugin/pom.xml ================================================ 4.0.0 org.sonarsource.sonarqube.tests base-plugin 0.1-SNAPSHOT sonar-plugin Base Plugin Fake plugin used to verify building of plugin classloaders org.sonarsource.api.plugin sonar-plugin-api 9.17.0.587 provided src org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin 1.15 true base org.sonar.plugins.base.BasePlugin ================================================ FILE: backend/plugin-commons/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java ================================================ package org.sonar.plugins.base; import org.sonar.api.Plugin; import org.sonar.api.Plugin.Context; public class BasePlugin implements Plugin { @Override public void define(Context context) { // no extensions } } ================================================ FILE: backend/plugin-commons/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java ================================================ package org.sonar.plugins.base.api; public class BaseApi { public void doNothing() { } } ================================================ FILE: backend/plugin-commons/src/test/projects/classloader-leak-plugin/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core.plugins.tests classloader-leak-plugin 0.1-SNAPSHOT sonar-plugin Leak Plugin Fake plugin used to reproduce a JAR file leak when using URLClassloader 11 11 11 org.sonarsource.api.plugin sonar-plugin-api 9.17.0.587 provided org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin 1.15 true base org.sonar.plugins.leak.LeakPlugin ================================================ FILE: backend/plugin-commons/src/test/projects/classloader-leak-plugin/src/main/java/org/sonar/plugins/leak/LeakPlugin.java ================================================ package org.sonar.plugins.leak; import java.io.IOException; import org.sonar.api.Plugin; public class LeakPlugin implements Plugin { @Override public void define(Context context) { // See SLCORE-557 var resource = this.getClass().getClassLoader().getResource("Hello.txt"); // https://bugs.java.com/bugdatabase/view_bug?bug_id=JDK-8315993 try (var conn = resource.openConnection().getInputStream()) { conn.readAllBytes(); } catch (IOException e) { throw new RuntimeException(e); } // no extensions } } ================================================ FILE: backend/plugin-commons/src/test/projects/classloader-leak-plugin/src/main/resources/Hello.txt ================================================ Hello World ================================================ FILE: backend/plugin-commons/src/test/projects/dependent-plugin/pom.xml ================================================ 4.0.0 org.sonarsource.sonarqube.tests dependent-plugin 0.1-SNAPSHOT sonar-plugin Dependent Plugin Fake plugin used to verify that plugins can export some resources to other plugins org.sonarsource.api.plugin sonar-plugin-api 9.17.0.587 provided org.sonarsource.sonarqube.tests base-plugin 0.1-SNAPSHOT sonar-plugin provided src org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin 1.15 true dependent org.sonar.plugins.dependent.DependentPlugin ================================================ FILE: backend/plugin-commons/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java ================================================ package org.sonar.plugins.dependent; import org.sonar.api.Plugin; import org.sonar.api.Plugin.Context; import org.sonar.plugins.base.api.BaseApi; public class DependentPlugin implements Plugin { public DependentPlugin() { // uses a class that is exported by base-plugin new BaseApi().doNothing(); } @Override public void define(Context context) { // no extensions } } ================================================ FILE: backend/plugin-commons/src/test/projects/pom.xml ================================================ 4.0.0 org.sonarsource.sonarqube.tests parent 0.1-SNAPSHOT pom base-plugin dependent-plugin ================================================ FILE: backend/plugin-commons/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-parent 11.2-SNAPSHOT ../pom.xml sonarlint-backend-parent pom SonarLint Core - Backend analysis-engine cli commons core http plugin-api plugin-commons rpc-impl rule-extractor server-api server-connection telemetry ================================================ FILE: backend/rpc-impl/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-rpc-impl SonarLint Core - RPC Implementation Entry point for SonarLint RPC com.google.code.findbugs jsr305 provided org.eclipse.lsp4j org.eclipse.lsp4j.jsonrpc ${lsp4j.version} org.slf4j jul-to-slf4j ${project.groupId} sonarlint-rpc-protocol ${project.version} ${project.groupId} sonarlint-core ${project.version} ch.qos.logback logback-classic org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AbstractRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nullable; import org.slf4j.MDC; import org.sonarsource.sonarlint.core.SonarLintMDC; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.springframework.beans.factory.BeanFactory; abstract class AbstractRpcServiceDelegate { private final Supplier beanFactorySupplier; private final ExecutorServiceShutdownWatchable requestsExecutor; private final Executor requestAndNotificationsSequentialExecutor; private final Supplier logOutputSupplier; protected AbstractRpcServiceDelegate(SonarLintRpcServerImpl server) { this.beanFactorySupplier = server::getInitializedApplicationContext; this.requestsExecutor = server.getRequestsExecutor(); this.requestAndNotificationsSequentialExecutor = server.getRequestAndNotificationsSequentialExecutor(); this.logOutputSupplier = server::getLogOutput; } protected T getBean(Class clazz) { return beanFactorySupplier.get().getBean(clazz); } protected CompletableFuture requestAsync(Function code) { return requestAsync(code, null); } protected CompletableFuture requestAsync(Function code, @Nullable String configScopeId) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(requestsExecutor); // First we schedule the processing of the request on the sequential executor, to maintain ordering of notifications, requests, responses, and cancellations // We can maybe cancel early var sequentialFuture = CompletableFuture.runAsync(cancelMonitor::checkCanceled, requestAndNotificationsSequentialExecutor); // Then requests are processed asynchronously to not block the processing of notifications, responses and cancellations var requestFuture = sequentialFuture.thenApplyAsync(unused -> computeWithLogger(() -> { cancelMonitor.checkCanceled(); return code.apply(cancelMonitor); }, configScopeId), requestsExecutor); requestFuture.whenComplete((result, error) -> { if (error instanceof CancellationException) { cancelMonitor.cancel(); } }); return requestFuture; } protected CompletableFuture requestFutureAsync(Function> code, @Nullable String configScopeId) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(requestsExecutor); // First we schedule the processing of the request on the sequential executor, to maintain ordering of notifications, requests, responses, and cancellations // We can maybe cancel early var sequentialFuture = CompletableFuture.runAsync(cancelMonitor::checkCanceled, requestAndNotificationsSequentialExecutor); // Then requests are processed asynchronously to not block the processing of notifications, responses and cancellations var requestFuture = sequentialFuture.thenComposeAsync(unused -> computeWithLogger(() -> { cancelMonitor.checkCanceled(); return code.apply(cancelMonitor); }, configScopeId), requestsExecutor); requestFuture.whenComplete((result, error) -> { if (error instanceof CancellationException) { cancelMonitor.cancel(); } }); return requestFuture; } protected CompletableFuture runAsync(Consumer code, @Nullable String configScopeId) { var cancelMonitor = new SonarLintCancelMonitor(); cancelMonitor.watchForShutdown(requestsExecutor); // First we schedule the processing of the request on the sequential executor, to maintain ordering of notifications, requests, responses, and cancellations // We can maybe cancel early var sequentialFuture = CompletableFuture.runAsync(cancelMonitor::checkCanceled, requestAndNotificationsSequentialExecutor); // Then requests are processed asynchronously to not block the processing of notifications, responses and cancellations var requestFuture = sequentialFuture.thenApplyAsync(unused -> { doWithLogger(() -> { cancelMonitor.checkCanceled(); code.accept(cancelMonitor); }, configScopeId); return null; }, requestsExecutor); requestFuture.whenComplete((result, error) -> { if (error instanceof CancellationException) { cancelMonitor.cancel(); } }); return requestFuture; } /** * We don't want to risk a long notification to block the message processor thread and to prevent cancellation of requests, * so we are also moving notifications to a separate thread pool. Still we want to preserve ordering of requests and notifications. */ protected void notify(Runnable code) { notify(code, null); } protected void notify(Runnable code, @Nullable String configScopeId) { requestAndNotificationsSequentialExecutor.execute(() -> doWithLogger(() -> { try { code.run(); } catch (Throwable throwable) { SonarLintLogger.get().error("Error when handling notification", throwable); } }, configScopeId)); } private void doWithLogger(Runnable code, @Nullable String configScopeId) { SonarLintLogger.get().setTarget(logOutputSupplier.get()); SonarLintMDC.putConfigScopeId(configScopeId); logOutputSupplier.get().setConfigScopeId(configScopeId); try { code.run(); } finally { MDC.clear(); SonarLintLogger.get().setTarget(null); logOutputSupplier.get().setConfigScopeId(null); } } private G computeWithLogger(Supplier code, @Nullable String configScopeId) { SonarLintLogger.get().setTarget(logOutputSupplier.get()); SonarLintMDC.putConfigScopeId(configScopeId); logOutputSupplier.get().setConfigScopeId(configScopeId); try { return code.get(); } finally { MDC.clear(); SonarLintLogger.get().setTarget(null); logOutputSupplier.get().setConfigScopeId(null); } } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiAgentRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.ai.ide.AiAgentService; import org.sonarsource.sonarlint.core.ai.ide.AiHookService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgentRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetHookScriptContentParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetHookScriptContentResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetRuleFileContentParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.GetRuleFileContentResponse; public class AiAgentRpcServiceDelegate extends AbstractRpcServiceDelegate implements AiAgentRpcService { public AiAgentRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); } @Override public CompletableFuture getRuleFileContent(GetRuleFileContentParams params) { return requestAsync(cancelMonitor -> getBean(AiAgentService.class).getRuleFileContent(params.getAiAgent())); } @Override public CompletableFuture getHookScriptContent(GetHookScriptContentParams params) { return requestAsync(cancelMonitor -> getBean(AiHookService.class).getHookScriptContent(params.getAiAgent())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AiCodeFixRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.remediation.aicodefix.AiCodeFixService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixResponse; public class AiCodeFixRpcServiceDelegate extends AbstractRpcServiceDelegate implements AiCodeFixRpcService { public AiCodeFixRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); } @Override public CompletableFuture suggestFix(SuggestFixParams params) { return requestAsync(cancelMonitor -> getBean(AiCodeFixService.class).suggestFix(params.getConfigurationScopeId(), params.getIssueId(), cancelMonitor)); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/AnalysisRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.analysis.AnalysisResult; import org.sonarsource.sonarlint.core.analysis.AnalysisService; import org.sonarsource.sonarlint.core.analysis.NodeJsService; import org.sonarsource.sonarlint.core.analysis.RawIssue; import org.sonarsource.sonarlint.core.analysis.api.TriggerType; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFileListParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFilesAndTrackParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFilesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeFullProjectParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeOpenFilesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalyzeVCSChangedFilesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.DidChangeAnalysisPropertiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.DidChangeAutomaticAnalysisSettingParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.DidChangeClientNodeJsPathParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.DidChangePathToCompileCommandsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.ForceAnalyzeResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.GetAutoDetectedNodeJsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.GetForcedNodeJsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.GetSupportedFilePatternsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.GetSupportedFilePatternsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.NodeJsDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.ShouldUseEnterpriseCSharpAnalyzerParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.ShouldUseEnterpriseCSharpAnalyzerResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.FileEditDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.QuickFixDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.RawIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.RawIssueFlowDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.RawIssueLocationDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.TextEditDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto; import org.sonarsource.sonarlint.core.rules.RuleDetailsAdapter; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toMap; class AnalysisRpcServiceDelegate extends AbstractRpcServiceDelegate implements AnalysisRpcService { public AnalysisRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getSupportedFilePatterns(GetSupportedFilePatternsParams params) { return requestAsync( cancelChecker -> new GetSupportedFilePatternsResponse(getBean(AnalysisService.class).getSupportedFilePatterns(params.getConfigScopeId())), params.getConfigScopeId()); } @Override public CompletableFuture didChangeClientNodeJsPath(DidChangeClientNodeJsPathParams params) { return requestAsync(cancelChecker -> { var forcedNodeJs = getBean(NodeJsService.class).didChangeClientNodeJsPath(params.getClientNodeJsPath()); var dto = forcedNodeJs == null ? null : new NodeJsDetailsDto(forcedNodeJs.getPath(), forcedNodeJs.getVersion().toString()); return new GetForcedNodeJsResponse(dto); }); } @Override public CompletableFuture getAutoDetectedNodeJs() { return requestAsync(cancelChecker -> { var autoDetectedNodeJs = getBean(AnalysisService.class).getAutoDetectedNodeJs(); var dto = autoDetectedNodeJs == null ? null : new NodeJsDetailsDto(autoDetectedNodeJs.getPath(), autoDetectedNodeJs.getVersion().toString()); return new GetAutoDetectedNodeJsResponse(dto); }); } @Override public CompletableFuture analyzeFilesAndTrack(AnalyzeFilesAndTrackParams params) { var configurationScopeId = params.getConfigurationScopeId(); return requestFutureAsync(cancelChecker -> getBean(AnalysisService.class) .scheduleAnalysis(params.getConfigurationScopeId(), params.getAnalysisId(), Set.copyOf(params.getFilesToAnalyze()), params.getExtraProperties(), params.isShouldFetchServerIssues(), TriggerType.FORCED, cancelChecker) .thenApply(AnalysisRpcServiceDelegate::generateAnalyzeFilesResponse), configurationScopeId); } @Override public void didSetUserAnalysisProperties(DidChangeAnalysisPropertiesParams params) { notify(() -> getBean(AnalysisService.class).setUserAnalysisProperties(params.getConfigurationScopeId(), params.getProperties())); } @Override public void didChangePathToCompileCommands(DidChangePathToCompileCommandsParams params) { notify(() -> getBean(AnalysisService.class).didChangePathToCompileCommands(params.getConfigurationScopeId(), params.getPathToCompileCommands())); } @Override public void didChangeAutomaticAnalysisSetting(DidChangeAutomaticAnalysisSettingParams params) { notify(() -> getBean(AnalysisService.class).didChangeAutomaticAnalysisSetting(params.isEnabled())); } @Override public CompletableFuture analyzeFullProject(AnalyzeFullProjectParams params) { return requestAsync( cancelChecker -> new ForceAnalyzeResponse(getBean(AnalysisService.class) .analyzeFullProject(params.getConfigScopeId(), params.isHotspotsOnly()))); } @Override public CompletableFuture analyzeFileList(AnalyzeFileListParams params) { return requestAsync( cancelChecker -> new ForceAnalyzeResponse(getBean(AnalysisService.class) .analyzeFileList(params.getConfigScopeId(), params.getFilesToAnalyze()))); } @Override public CompletableFuture analyzeOpenFiles(AnalyzeOpenFilesParams params) { return requestAsync( cancelChecker -> new ForceAnalyzeResponse(getBean(AnalysisService.class).forceAnalyzeOpenFiles(params.getConfigScopeId()))); } @Override public CompletableFuture analyzeVCSChangedFiles(AnalyzeVCSChangedFilesParams params) { return requestAsync( cancelChecker -> new ForceAnalyzeResponse(getBean(AnalysisService.class).analyzeVCSChangedFiles(params.getConfigScopeId()))); } @Override public CompletableFuture shouldUseEnterpriseCSharpAnalyzer(ShouldUseEnterpriseCSharpAnalyzerParams params) { return requestAsync( cancelChecker -> new ShouldUseEnterpriseCSharpAnalyzerResponse(getBean(AnalysisService.class) .shouldUseEnterpriseCSharpAnalyzer(params.getConfigurationScopeId()))); } private static AnalyzeFilesResponse generateAnalyzeFilesResponse(AnalysisResult analysisResults) { return new AnalyzeFilesResponse(analysisResults.failedAnalysisFiles(), analysisResults.rawIssues().stream().map(AnalysisRpcServiceDelegate::toDto).toList()); } static RawIssueDto toDto(RawIssue issue) { var range = issue.getTextRange(); var textRange = range != null ? adapt(range) : null; var fileUri = issue.getFileUri(); var flows = issue.getFlows().stream().map(flow -> { var locations = flow.locations().stream().map(location -> { var locationTextRange = location.getTextRange(); var locationTextRangeDto = locationTextRange == null ? null : adapt(locationTextRange); var locationInputFile = location.getInputFile(); var locationFileUri = locationInputFile == null ? null : locationInputFile.uri(); return new RawIssueLocationDto(locationTextRangeDto, location.getMessage(), locationFileUri); }).toList(); return new RawIssueFlowDto(locations); }).toList(); return new RawIssueDto( RuleDetailsAdapter.adapt(issue.getSeverity()), RuleDetailsAdapter.adapt(issue.getRuleType()), RuleDetailsAdapter.adapt(issue.getCleanCodeAttribute()), issue.getImpacts().entrySet().stream().map(entry -> Map.entry(SoftwareQuality.valueOf(entry.getKey().name()), ImpactSeverity.valueOf(entry.getValue().name()))) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)), issue.getRuleKey(), requireNonNull(issue.getMessage()), fileUri, flows, issue.getQuickFixes().stream() .map(quickFix -> new QuickFixDto( quickFix.inputFileEdits().stream() .map(fileEdit -> new FileEditDto(fileEdit.target().uri(), fileEdit.textEdits().stream().map(textEdit -> new TextEditDto(adapt(textEdit.range()), textEdit.newText())).toList())) .toList(), quickFix.message())) .toList(), textRange, issue.getRuleDescriptionContextKey(), RuleDetailsAdapter.adapt(issue.getVulnerabilityProbability())); } private static TextRangeDto adapt(TextRange textRange) { return new TextRangeDto(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/BackendJsonRpcLauncher.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.io.Closeable; import java.io.InputStream; import java.io.OutputStream; public class BackendJsonRpcLauncher implements Closeable { private final SonarLintRpcServerImpl server; public BackendJsonRpcLauncher(InputStream in, OutputStream out) { server = new SonarLintRpcServerImpl(in, out); } public SonarLintRpcServerImpl getServer() { return server; } /** * @deprecated All related codes moved to org.sonarsource.sonarlint.core.rpc.impl.SonarLintRpcServerImpl#shutdown() * Calling server shutdown method is enough. */ @Override @Deprecated(since = "10.4", forRemoval = true) public void close() { // This method is used by the language server. It will be removed once the usage has been removed } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/BindingRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.BindingSuggestionProvider; import org.sonarsource.sonarlint.core.SharedConnectedModeSettingsProvider; import org.sonarsource.sonarlint.core.commons.SonarLintException; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.BindingRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.GetBindingSuggestionParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.GetSharedConnectedModeConfigFileParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.GetSharedConnectedModeConfigFileResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.GetBindingSuggestionsResponse; class BindingRpcServiceDelegate extends AbstractRpcServiceDelegate implements BindingRpcService { public BindingRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getBindingSuggestions(GetBindingSuggestionParams params) { return requestAsync( cancelMonitor -> new GetBindingSuggestionsResponse( getBean(BindingSuggestionProvider.class).getBindingSuggestions(params.getConfigScopeId(), params.getConnectionId(), cancelMonitor)), params.getConfigScopeId()); } @Override public CompletableFuture getSharedConnectedModeConfigFileContents(GetSharedConnectedModeConfigFileParams params) { return requestAsync(cancelMonitor -> { try { return new GetSharedConnectedModeConfigFileResponse( getBean(SharedConnectedModeSettingsProvider.class).getSharedConnectedModeConfigFileContents(params.getConfigScopeId())); } catch (SonarLintException e) { var error = new ResponseError(SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND, e.getMessage(), params.getConfigScopeId()); throw new ResponseErrorException(error); } }, params.getConfigScopeId()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/ConfigurationRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import org.sonarsource.sonarlint.core.ConfigurationService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidAddConfigurationScopesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidRemoveConfigurationScopeParams; class ConfigurationRpcServiceDelegate extends AbstractRpcServiceDelegate implements ConfigurationRpcService { public ConfigurationRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public void didAddConfigurationScopes(DidAddConfigurationScopesParams params) { notify(() -> getBean(ConfigurationService.class).didAddConfigurationScopes(params.getAddedScopes())); } @Override public void didRemoveConfigurationScope(DidRemoveConfigurationScopeParams params) { notify(() -> getBean(ConfigurationService.class).didRemoveConfigurationScope(params.getRemovedId())); } @Override public void didUpdateBinding(DidUpdateBindingParams params) { notify(() -> getBean(ConfigurationService.class).didUpdateBinding(params.getConfigScopeId(), params.getUpdatedBinding() )); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/ConnectionRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.ConnectionService; import org.sonarsource.sonarlint.core.ConnectionSuggestionProvider; import org.sonarsource.sonarlint.core.MCPServerConfigurationProvider; import org.sonarsource.sonarlint.core.OrganizationsCache; import org.sonarsource.sonarlint.core.SonarProjectsCache; import org.sonarsource.sonarlint.core.commons.SonarLintException; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.GetConnectionSuggestionsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.GetMCPServerConfigurationParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.GetMCPServerConfigurationResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.auth.HelpGenerateUserTokenResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.DidChangeCredentialsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.DidUpdateConnectionsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.FuzzySearchUserOrganizationsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.FuzzySearchUserOrganizationsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.GetOrganizationParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.GetOrganizationResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.ListUserOrganizationsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.ListUserOrganizationsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.FuzzySearchProjectsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.FuzzySearchProjectsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetAllProjectsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetAllProjectsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetProjectNamesByKeyParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetProjectNamesByKeyResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.validate.ValidateConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.validate.ValidateConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetConnectionSuggestionsParams; class ConnectionRpcServiceDelegate extends AbstractRpcServiceDelegate implements ConnectionRpcService { public ConnectionRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public void didUpdateConnections(DidUpdateConnectionsParams params) { notify(() -> getBean(ConnectionService.class).didUpdateConnections(params.getSonarQubeConnections(), params.getSonarCloudConnections())); } @Override public void didChangeCredentials(DidChangeCredentialsParams params) { notify(() -> getBean(ConnectionService.class).didChangeCredentials(params.getConnectionId())); } @Override public CompletableFuture helpGenerateUserToken(HelpGenerateUserTokenParams params) { return requestAsync(cancelMonitor -> getBean(ConnectionService.class).helpGenerateUserToken(params.getServerUrl(), params.getUtm(), cancelMonitor)); } @Override public CompletableFuture validateConnection(ValidateConnectionParams params) { return requestAsync(cancelMonitor -> getBean(ConnectionService.class).validateConnection(params.getTransientConnection(), cancelMonitor)); } @Override public CompletableFuture listUserOrganizations(ListUserOrganizationsParams params) { return requestAsync(cancelMonitor -> new ListUserOrganizationsResponse(getBean(OrganizationsCache.class) .listUserOrganizations(new TransientSonarCloudConnectionDto(null, params.getCredentials(), params.getRegion()), cancelMonitor))); } @Override public CompletableFuture getOrganization(GetOrganizationParams params) { return requestAsync(cancelMonitor -> new GetOrganizationResponse(getBean(OrganizationsCache.class) .getOrganization(new TransientSonarCloudConnectionDto(params.getOrganizationKey(), params.getCredentials(), params.getRegion()), cancelMonitor))); } @Override public CompletableFuture fuzzySearchUserOrganizations(FuzzySearchUserOrganizationsParams params) { return requestAsync(cancelMonitor -> new FuzzySearchUserOrganizationsResponse(getBean(OrganizationsCache.class) .fuzzySearchOrganizations(new TransientSonarCloudConnectionDto(null, params.getCredentials(), params.getRegion()), params.getSearchText(), cancelMonitor))); } @Override public CompletableFuture getAllProjects(GetAllProjectsParams params) { return requestAsync(cancelMonitor -> new GetAllProjectsResponse(getBean(ConnectionService.class).getAllProjects(params.getTransientConnection(), cancelMonitor))); } @Override public CompletableFuture fuzzySearchProjects(FuzzySearchProjectsParams params) { return requestAsync(cancelMonitor -> new FuzzySearchProjectsResponse(getBean(SonarProjectsCache.class) .fuzzySearchProjects(params.getConnectionId(), params.getSearchText(), cancelMonitor))); } @Override public CompletableFuture getProjectNamesByKey(GetProjectNamesByKeyParams params) { return requestAsync(cancelMonitor -> new GetProjectNamesByKeyResponse(getBean(ConnectionService.class) .getProjectNamesByKey(params.getTransientConnection(), params.getProjectKeys(), cancelMonitor))); } @Override public CompletableFuture getConnectionSuggestions(GetConnectionSuggestionsParams params) { return requestAsync( cancelMonitor -> new GetConnectionSuggestionsResponse(getBean(ConnectionSuggestionProvider.class) .getConnectionSuggestions(params.getConfigurationScopeId(), cancelMonitor))); } @Override public CompletableFuture getMCPServerConfiguration(GetMCPServerConfigurationParams params) { return requestAsync(cancelMonitor -> { try { return new GetMCPServerConfigurationResponse( getBean(MCPServerConfigurationProvider.class).getMCPServerConfigurationJSON(params.getConnectionId(), params.getToken())); } catch (SonarLintException e) { var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, e.getMessage(), params.getConnectionId()); throw new ResponseErrorException(error); } }); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/DependencyRiskRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.ChangeDependencyRiskStatusParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.CheckDependencyRiskSupportedParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.CheckDependencyRiskSupportedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.ListAllDependencyRisksResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.OpenDependencyRiskInBrowserParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ListAllParams; import org.sonarsource.sonarlint.core.sca.DependencyRiskService; public class DependencyRiskRpcServiceDelegate extends AbstractRpcServiceDelegate implements DependencyRiskRpcService { public DependencyRiskRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture listAll(ListAllParams params) { return requestAsync(cancelMonitor -> new ListAllDependencyRisksResponse(getBean(DependencyRiskService.class) .listAll(params.getConfigurationScopeId(), params.shouldRefresh(), cancelMonitor))); } @Override public CompletableFuture changeStatus(ChangeDependencyRiskStatusParams params) { return runAsync(cancelMonitor -> { try { getBean(DependencyRiskService.class).changeStatus( params.getConfigurationScopeId(), params.getDependencyRiskKey(), params.getTransition(), params.getComment(), cancelMonitor); } catch (DependencyRiskService.DependencyRiskNotFoundException e) { var error = new ResponseError(SonarLintRpcErrorCode.ISSUE_NOT_FOUND, "Dependency Risk with key " + e.getKey() + " was not found", e.getKey()); throw new ResponseErrorException(error); } catch (IllegalArgumentException e) { var error = new ResponseError(SonarLintRpcErrorCode.INVALID_ARGUMENT, e.getMessage(), null); throw new ResponseErrorException(error); } }, params.getConfigurationScopeId()); } @Override public CompletableFuture openDependencyRiskInBrowser(OpenDependencyRiskInBrowserParams params) { return runAsync(cancelMonitor -> { try { getBean(DependencyRiskService.class).openDependencyRiskInBrowser( params.getConfigScopeId(), params.getDependencyRiskKey()); } catch (IllegalArgumentException e) { var error = new ResponseError(SonarLintRpcErrorCode.INVALID_ARGUMENT, e.getMessage(), null); throw new ResponseErrorException(error); } }, params.getConfigScopeId()); } @Override public CompletableFuture checkSupported(CheckDependencyRiskSupportedParams params) { return requestAsync(cancelMonitor -> getBean(DependencyRiskService.class).checkSupported(params.getConfigurationScopeId()), params.getConfigurationScopeId()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/DogfoodingRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.commons.dogfood.DogfoodEnvironmentDetectionService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.dogfooding.DogfoodingRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.dogfooding.IsDogfoodingEnvironmentResponse; public class DogfoodingRpcServiceDelegate extends AbstractRpcServiceDelegate implements DogfoodingRpcService { private final DogfoodEnvironmentDetectionService dogfoodEnvironmentDetectionService; public DogfoodingRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); this.dogfoodEnvironmentDetectionService = new DogfoodEnvironmentDetectionService(); } @Override public CompletableFuture isDogfoodingEnvironment() { return requestAsync(cancelMonitor -> new IsDogfoodingEnvironmentResponse(dogfoodEnvironmentDetectionService.isDogfoodEnvironment())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/FileRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.fs.ClientFileSystemService; import org.sonarsource.sonarlint.core.fs.FileExclusionService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidCloseFileParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidOpenFileParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidUpdateFileSystemParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.FileRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.GetFilesStatusParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.GetFilesStatusResponse; public class FileRpcServiceDelegate extends AbstractRpcServiceDelegate implements FileRpcService { protected FileRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getFilesStatus(GetFilesStatusParams params) { return requestAsync(cancelChecker -> { var statuses = getBean(FileExclusionService.class).getFilesStatus(params.getFileUrisByConfigScopeId()); return new GetFilesStatusResponse(statuses); }); } @Override public void didUpdateFileSystem(DidUpdateFileSystemParams params) { notify(() -> getBean(ClientFileSystemService.class).didUpdateFileSystem(params)); } @Override public void didOpenFile(DidOpenFileParams params) { notify(() -> getBean(ClientFileSystemService.class).didOpenFile(params.getConfigurationScopeId(), params.getFileUri())); } @Override public void didCloseFile(DidCloseFileParams params) { notify(() -> getBean(ClientFileSystemService.class).didCloseFile(params.getConfigurationScopeId(), params.getFileUri())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/HotspotRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.hotspot.HotspotService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.ChangeHotspotStatusParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckLocalDetectionSupportedParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckLocalDetectionSupportedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckStatusChangePermittedParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.CheckStatusChangePermittedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.OpenHotspotInBrowserParams; class HotspotRpcServiceDelegate extends AbstractRpcServiceDelegate implements HotspotRpcService { public HotspotRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public void openHotspotInBrowser(OpenHotspotInBrowserParams params) { notify(() -> getBean(HotspotService.class).openHotspotInBrowser(params.getConfigScopeId(), params.getHotspotKey()), params.getConfigScopeId()); } @Override public CompletableFuture checkLocalDetectionSupported(CheckLocalDetectionSupportedParams params) { return requestAsync(cancelChecker -> getBean(HotspotService.class).checkLocalDetectionSupported(params.getConfigScopeId()), params.getConfigScopeId()); } @Override public CompletableFuture checkStatusChangePermitted(CheckStatusChangePermittedParams params) { return requestAsync(cancelChecker -> getBean(HotspotService.class).checkStatusChangePermitted(params.getConnectionId(), params.getHotspotKey(), cancelChecker)); } @Override public CompletableFuture changeStatus(ChangeHotspotStatusParams params) { return runAsync( cancelMonitor -> getBean(HotspotService.class).changeStatus(params.getConfigurationScopeId(), params.getHotspotKey(), adapt(params.getNewStatus()), cancelMonitor), params.getConfigurationScopeId()); } private static HotspotReviewStatus adapt(HotspotStatus newStatus) { return HotspotReviewStatus.valueOf(newStatus.name()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/IdeLabsRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.labs.IdeLabsService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.IdeLabsRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.JoinIdeLabsProgramParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.JoinIdeLabsProgramResponse; public class IdeLabsRpcServiceDelegate extends AbstractRpcServiceDelegate implements IdeLabsRpcService { public IdeLabsRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture joinIdeLabsProgram(JoinIdeLabsProgramParams params) { return requestAsync(cancelChecker -> getBean(IdeLabsService.class).joinIdeLabsProgram(params.getEmail(), params.getIde())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/IssueRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.issue.IssueNotFoundException; import org.sonarsource.sonarlint.core.issue.IssueService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.AddIssueCommentParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ChangeIssueStatusParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.CheckAnticipatedStatusChangeSupportedParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.CheckAnticipatedStatusChangeSupportedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.CheckStatusChangePermittedParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.CheckStatusChangePermittedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.GetEffectiveIssueDetailsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.GetEffectiveIssueDetailsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.IssueRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenAllIssuesForFileParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenAllIssuesForFileResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenIssueParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ReopenIssueResponse; import org.sonarsource.sonarlint.core.rules.RuleNotFoundException; public class IssueRpcServiceDelegate extends AbstractRpcServiceDelegate implements IssueRpcService { public IssueRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture changeStatus(ChangeIssueStatusParams params) { return runAsync(cancelMonitor -> getBean(IssueService.class).changeStatus(params.getConfigurationScopeId(), params.getIssueKey(), params.getNewStatus(), params.isTaintIssue(), cancelMonitor), params.getConfigurationScopeId()); } @Override public CompletableFuture addComment(AddIssueCommentParams params) { return runAsync(cancelMonitor -> getBean(IssueService.class).addComment(params.getConfigurationScopeId(), params.getIssueKey(), params.getText(), cancelMonitor), params.getConfigurationScopeId()); } @Override public CompletableFuture checkAnticipatedStatusChangeSupported(CheckAnticipatedStatusChangeSupportedParams params) { return requestAsync(cancelMonitor -> new CheckAnticipatedStatusChangeSupportedResponse( getBean(IssueService.class).checkAnticipatedStatusChangeSupported(params.getConfigScopeId())), params.getConfigScopeId()); } @Override public CompletableFuture checkStatusChangePermitted(CheckStatusChangePermittedParams params) { return requestAsync(cancelMonitor -> getBean(IssueService.class).checkStatusChangePermitted(params.getConnectionId(), params.getIssueKey(), cancelMonitor)); } @Override public CompletableFuture reopenIssue(ReopenIssueParams params) { return requestAsync( cancelMonitor -> new ReopenIssueResponse( getBean(IssueService.class).reopenIssue(params.getConfigurationScopeId(), params.getIssueId(), params.isTaintIssue(), cancelMonitor)), params.getConfigurationScopeId()); } @Override public CompletableFuture reopenAllIssuesForFile(ReopenAllIssuesForFileParams params) { return requestAsync(cancelMonitor -> new ReopenAllIssuesForFileResponse(getBean(IssueService.class).reopenAllIssuesForFile(params, cancelMonitor)), params.getConfigurationScopeId()); } @Override public CompletableFuture getEffectiveIssueDetails(GetEffectiveIssueDetailsParams params) { return requestAsync(cancelMonitor -> { try { return new GetEffectiveIssueDetailsResponse(getBean(IssueService.class) .getEffectiveIssueDetails(params.getConfigurationScopeId(), params.getIssueId(), cancelMonitor)); } catch (IssueNotFoundException e) { var error = new ResponseError(SonarLintRpcErrorCode.ISSUE_NOT_FOUND, e.getMessage(), e.getIssueKey()); throw new ResponseErrorException(error); } catch (RuleNotFoundException e) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, e.getMessage(), e.getRuleKey()); throw new ResponseErrorException(error); } }); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/LogServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import org.sonarsource.sonarlint.core.log.LogService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.log.LogRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.log.SetLogLevelParams; public class LogServiceDelegate extends AbstractRpcServiceDelegate implements LogRpcService { public LogServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); } @Override public void setLogLevel(SetLogLevelParams params) { notify(() -> getBean(LogService.class).setLogLevel(params.getNewLevel())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/NewCodeRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.newcode.NewCodeService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.GetNewCodeDefinitionParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.GetNewCodeDefinitionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.NewCodeRpcService; public class NewCodeRpcServiceDelegate extends AbstractRpcServiceDelegate implements NewCodeRpcService { public NewCodeRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getNewCodeDefinition(GetNewCodeDefinitionParams params) { return requestAsync(cancelMonitor -> getBean(NewCodeService.class).getNewCodeDefinition(params.getConfigScopeId()), params.getConfigScopeId()); } @Override public void didToggleFocus() { notify(() -> getBean(NewCodeService.class).didToggleFocus()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/PluginRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.Binding; import org.sonarsource.sonarlint.core.plugin.PluginStatusMapper; import org.sonarsource.sonarlint.core.plugin.PluginsService; import org.sonarsource.sonarlint.core.repository.config.ConfigurationRepository; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.GetPluginStatusesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.GetPluginStatusesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginRpcService; public class PluginRpcServiceDelegate extends AbstractRpcServiceDelegate implements PluginRpcService { public PluginRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getPluginStatuses(GetPluginStatusesParams params) { return requestAsync(cancelMonitor -> { var configScopeId = params.getConfigurationScopeId(); var connectionId = resolveConnectionId(configScopeId); var statuses = getBean(PluginsService.class).getPluginStatuses(connectionId); return new GetPluginStatusesResponse(PluginStatusMapper.toDto(statuses)); }, params.getConfigurationScopeId()); } @Nullable private String resolveConnectionId(@Nullable String configurationScopeId) { if (configurationScopeId == null) { return null; } return getBean(ConfigurationRepository.class) .getEffectiveBinding(configurationScopeId) .map(Binding::connectionId) .orElse(null); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/RpcClientLogOutput.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.time.Instant; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.LogOutput; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogLevel; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; class RpcClientLogOutput implements LogOutput { private final SonarLintRpcClient client; private final InheritableThreadLocal configScopeId = new InheritableThreadLocal<>(); RpcClientLogOutput(SonarLintRpcClient client) { this.client = client; } @Override public void log(@Nullable String msg, Level level, @Nullable String stacktrace) { client.log(new LogParams(LogLevel.valueOf(level.name()), msg, configScopeId.get(), stacktrace, Instant.now())); } public void setConfigScopeId(@Nullable String configScopeId) { this.configScopeId.set(configScopeId); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/RulesRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.sonarsource.sonarlint.core.active.rules.ActiveRulesService; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetStandaloneRuleDescriptionParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetStandaloneRuleDescriptionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.ListAllStandaloneRulesDefinitionsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RulesRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.UpdateStandaloneRulesConfigurationParams; import org.sonarsource.sonarlint.core.rules.RuleNotFoundException; import org.sonarsource.sonarlint.core.rules.RulesService; class RulesRpcServiceDelegate extends AbstractRpcServiceDelegate implements RulesRpcService { public RulesRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getEffectiveRuleDetails(GetEffectiveRuleDetailsParams params) { return requestAsync(cancelMonitor -> { try { return new GetEffectiveRuleDetailsResponse( getBean(ActiveRulesService.class).getEffectiveRuleDetails(params.getConfigurationScopeId(), params.getRuleKey(), params.getContextKey(), cancelMonitor)); } catch (RuleNotFoundException e) { var error = new ResponseError(SonarLintRpcErrorCode.RULE_NOT_FOUND, e.getMessage(), e.getRuleKey()); throw new ResponseErrorException(error); } }, params.getConfigurationScopeId()); } @Override public CompletableFuture listAllStandaloneRulesDefinitions() { return requestAsync(cancelMonitor -> new ListAllStandaloneRulesDefinitionsResponse(getBean(RulesService.class).listAllStandaloneRulesDefinitions())); } @Override public CompletableFuture getStandaloneRuleDetails(GetStandaloneRuleDescriptionParams params) { return requestAsync(cancelMonitor -> getBean(ActiveRulesService.class).getStandaloneRuleDescription(params.getRuleKey())); } @Override public void updateStandaloneRulesConfiguration(UpdateStandaloneRulesConfigurationParams params) { notify(() -> getBean(ActiveRulesService.class).updateStandaloneRulesConfiguration(params.getRuleConfigByKey())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcClientLogbackAppender.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.core.AppenderBase; import java.time.Instant; import org.sonarsource.sonarlint.core.SonarLintMDC; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogLevel; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; class SonarLintRpcClientLogbackAppender extends AppenderBase { private final SonarLintRpcClient rpcClient; private final ThrowableProxyConverter tpc = new ThrowableProxyConverter(); public SonarLintRpcClientLogbackAppender(SonarLintRpcClient client) { rpcClient = client; } @Override public void start() { tpc.start(); super.start(); } @Override protected void append(ILoggingEvent eventObject) { var configScopeId = eventObject.getMDCPropertyMap().get(SonarLintMDC.CONFIG_SCOPE_ID_MDC_KEY); var threadName = eventObject.getThreadName(); var loggerName = eventObject.getLoggerName(); var formattedMessage = eventObject.getFormattedMessage(); var loggedAt = Instant.ofEpochMilli(eventObject.getTimeStamp()); IThrowableProxy tp = eventObject.getThrowableProxy(); String stackTrace = null; if (tp != null) { stackTrace = tpc.convert(eventObject); } rpcClient.log(new LogParams(LogLevel.valueOf(eventObject.getLevel().levelStr), formattedMessage, configScopeId, threadName, loggerName, stackTrace, loggedAt)); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImpl.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import ch.qos.logback.classic.Level; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import jetbrains.exodus.core.execution.JobProcessor; import jetbrains.exodus.core.execution.ThreadJobProcessorPool; import org.eclipse.lsp4j.jsonrpc.CompletableFutures; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.ExecutorServiceShutdownWatchable; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.embedded.server.EmbeddedServer; import org.sonarsource.sonarlint.core.log.LogService; import org.sonarsource.sonarlint.core.rpc.protocol.RpcErrorHandler; import org.sonarsource.sonarlint.core.rpc.protocol.SingleThreadedMessageConsumer; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintLauncherBuilder; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgentRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.AnalysisRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.binding.BindingRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.SonarProjectBranchRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.ConfigurationRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.ConnectionRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.dogfooding.DogfoodingRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.FileRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.IssueRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.IdeLabsRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.log.LogRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.newcode.NewCodeRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.progress.TaskProgressRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RulesRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.sca.DependencyRiskRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityTrackingRpcService; import org.sonarsource.sonarlint.core.serverapi.exception.ServerRequestException; import org.sonarsource.sonarlint.core.serverconnection.issues.LocalOnlyIssuesRepository; import org.sonarsource.sonarlint.core.spring.SpringApplicationContextInitializer; import org.sonarsource.sonarlint.core.storage.StorageService; import org.springframework.context.ConfigurableApplicationContext; public class SonarLintRpcServerImpl implements SonarLintRpcServer { private static final Logger LOG = LoggerFactory.getLogger(SonarLintRpcServerImpl.class); private final SonarLintRpcClient client; private final AtomicBoolean initializeCalled = new AtomicBoolean(false); private final AtomicBoolean initialized = new AtomicBoolean(false); private final Future clientListener; private final ExecutorServiceShutdownWatchable requestsExecutor; private final ExecutorService requestAndNotificationsSequentialExecutor; private final RpcClientLogOutput logOutput; private final ExecutorService messageReaderExecutor; private final ExecutorService messageWriterExecutor; private SpringApplicationContextInitializer springApplicationContextInitializer; public SonarLintRpcServerImpl(InputStream in, OutputStream out) { this.messageReaderExecutor = Executors.newCachedThreadPool(r -> { var t = new Thread(r); t.setName("Server message reader"); return t; }); this.messageWriterExecutor = Executors.newCachedThreadPool(r -> { var t = new Thread(r); t.setName("Server message writer"); return t; }); this.requestAndNotificationsSequentialExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "SonarLint Server RPC sequential executor")); this.requestsExecutor = new ExecutorServiceShutdownWatchable<>(Executors.newCachedThreadPool(r -> new Thread(r, "SonarLint Server RPC request executor"))); var launcher = new SonarLintLauncherBuilder() .setLocalService(this) .setRemoteInterface(SonarLintRpcClient.class) .setInput(in) .setOutput(out) .setExecutorService(messageReaderExecutor) .wrapMessages(m -> new SingleThreadedMessageConsumer(m, messageWriterExecutor, System.err::println)) .traceMessages(getMessageTracer()) .setExceptionHandler(this::handleError) .create(); this.client = launcher.getRemoteProxy(); this.logOutput = new RpcClientLogOutput(client); // Remove existing handlers attached to j.u.l root logger SLF4JBridgeHandler.removeHandlersForRootLogger(); // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during // the initialization phase of your application SLF4JBridgeHandler.install(); var rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.detachAndStopAllAppenders(); var rpcAppender = new SonarLintRpcClientLogbackAppender(client); rpcAppender.start(); rootLogger.addAppender(rpcAppender); this.clientListener = launcher.startListening(); } private ResponseError handleError(Throwable throwable) { if (shouldSkipExceptionCapture(throwable)) { return new ResponseError(ResponseErrorCode.RequestFailed, throwable.getMessage(), toStringStacktrace(throwable)); } return RpcErrorHandler.handleError(throwable); } private static boolean shouldSkipExceptionCapture(Throwable throwable) { return throwable instanceof ServerRequestException || (throwable instanceof CompletionException && throwable.getCause() instanceof ServerRequestException); } private static String toStringStacktrace(Throwable throwable) { var sw = new java.io.StringWriter(); throwable.printStackTrace(new PrintWriter(sw)); return sw.toString(); } private static PrintWriter getMessageTracer() { if ("true".equals(System.getProperty("sonarlint.debug.rpc"))) { try { return new PrintWriter(Paths.get(System.getProperty("user.home")).resolve(".sonarlint").resolve("rpc_backend_session.log").toFile(), StandardCharsets.UTF_8); } catch (IOException e) { System.err.println("Cannot write rpc debug logs file"); e.printStackTrace(); } } return null; } public Future getClientListener() { return clientListener; } @Override public CompletableFuture initialize(InitializeParams params) { return CompletableFutures.computeAsync(requestAndNotificationsSequentialExecutor, cancelChecker -> { SonarLintLogger.get().setLevel(LogService.convert(params.getLogLevel())); SonarLintLogger.get().setTarget(logOutput); // for flyway logging level setLogbackRootLogger(params); if (initializeCalled.compareAndSet(false, true) && !initialized.get()) { springApplicationContextInitializer = new SpringApplicationContextInitializer(client, params); initialized.set(true); } else { var error = new ResponseError(SonarLintRpcErrorCode.BACKEND_ALREADY_INITIALIZED, "Backend already initialized", null); throw new ResponseErrorException(error); } return null; }); } private static void setLogbackRootLogger(InitializeParams params) { var root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); var logLevel = switch (params.getLogLevel()) { case OFF -> Level.OFF; case ERROR -> Level.ERROR; case WARN -> Level.WARN; case INFO -> Level.INFO; case DEBUG -> Level.DEBUG; case TRACE -> Level.TRACE; }; root.setLevel(logLevel); } public ConfigurableApplicationContext getInitializedApplicationContext() { if (!initialized.get()) { throw new IllegalStateException("Backend is not initialized"); } return springApplicationContextInitializer.getInitializedApplicationContext(); } @Override public ConnectionRpcService getConnectionService() { return new ConnectionRpcServiceDelegate(this); } @Override public ConfigurationRpcService getConfigurationService() { return new ConfigurationRpcServiceDelegate(this); } @Override public FileRpcService getFileService() { return new FileRpcServiceDelegate(this); } @Override public HotspotRpcService getHotspotService() { return new HotspotRpcServiceDelegate(this); } @Override public TelemetryRpcService getTelemetryService() { return new TelemetryRpcServiceDelegate(this); } @Override public AnalysisRpcService getAnalysisService() { return new AnalysisRpcServiceDelegate(this); } @Override public RulesRpcService getRulesService() { return new RulesRpcServiceDelegate(this); } @Override public BindingRpcService getBindingService() { return new BindingRpcServiceDelegate(this); } public SonarProjectBranchRpcService getSonarProjectBranchService() { return new SonarProjectBranchRpcServiceDelegate(this); } @Override public IssueRpcService getIssueService() { return new IssueRpcServiceDelegate(this); } @Override public NewCodeRpcService getNewCodeService() { return new NewCodeRpcServiceDelegate(this); } @Override public TaintVulnerabilityTrackingRpcService getTaintVulnerabilityTrackingService() { return new TaintVulnerabilityTrackingRpcServiceDelegate(this); } @Override public DogfoodingRpcService getDogfoodingService() { return new DogfoodingRpcServiceDelegate(this); } @Override public AiCodeFixRpcService getAiCodeFixRpcService() { return new AiCodeFixRpcServiceDelegate(this); } @Override public TaskProgressRpcService getTaskProgressRpcService() { return new TaskProgressRpcServiceDelegate(this); } @Override public DependencyRiskRpcService getDependencyRiskService() { return new DependencyRiskRpcServiceDelegate(this); } @Override public AiAgentRpcService getAiAgentService() { return new AiAgentRpcServiceDelegate(this); } @Override public LogRpcService getLogService() { return new LogServiceDelegate(this); } @Override public IdeLabsRpcService getIdeLabsService() { return new IdeLabsRpcServiceDelegate(this); } @Override public PluginRpcService getPluginService() { return new PluginRpcServiceDelegate(this); } @Override public CompletableFuture shutdown() { LOG.info("SonarLint backend shutting down, instance={}", this); var executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "SonarLint Server shutdown")); CompletableFuture future = CompletableFutures.computeAsync(executor, cancelChecker -> { SonarLintLogger.get().setTarget(logOutput); var wasInitialized = initialized.getAndSet(false); MoreExecutors.shutdownAndAwaitTermination(requestsExecutor, 1, TimeUnit.SECONDS); MoreExecutors.shutdownAndAwaitTermination(requestAndNotificationsSequentialExecutor, 1, TimeUnit.SECONDS); if (wasInitialized) { try { springApplicationContextInitializer.close(); } catch (Exception e) { SonarLintLogger.get().error("Error while closing Spring context", e); } } ThreadJobProcessorPool.getProcessors().forEach(JobProcessor::finish); shutdownReaderAndWriter(); return null; }); executor.shutdown(); return future; } public void shutdownReaderAndWriter() { messageReaderExecutor.shutdownNow(); // shutdown writer and disconnect from client asynchronously to make sure the client gets the response var scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); try { scheduledExecutorService.schedule(() -> { messageWriterExecutor.shutdownNow(); disconnectFromClient(); }, 1, TimeUnit.SECONDS); } finally { scheduledExecutorService.shutdown(); } } private void disconnectFromClient() { clientListener.cancel(true); } public boolean isReaderShutdown() { return messageReaderExecutor.isShutdown(); } public int getEmbeddedServerPort() { return getInitializedApplicationContext().getBean(EmbeddedServer.class).getPort(); } public StorageService getIssueStorageService() { return getInitializedApplicationContext().getBean(StorageService.class); } public LocalOnlyIssuesRepository getLocalOnlyIssuesRepository() { return getInitializedApplicationContext().getBean(LocalOnlyIssuesRepository.class); } public SonarLintDatabase getDatabase() { return getInitializedApplicationContext().getBean(SonarLintDatabase.class); } ExecutorServiceShutdownWatchable getRequestsExecutor() { return requestsExecutor; } ExecutorService getRequestAndNotificationsSequentialExecutor() { return requestAndNotificationsSequentialExecutor; } RpcClientLogOutput getLogOutput() { return logOutput; } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/SonarProjectBranchRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.branch.SonarProjectBranchTrackingService; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.DidVcsRepositoryChangeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.GetMatchedSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.GetMatchedSonarProjectBranchResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.SonarProjectBranchRpcService; class SonarProjectBranchRpcServiceDelegate extends AbstractRpcServiceDelegate implements SonarProjectBranchRpcService { public SonarProjectBranchRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public void didVcsRepositoryChange(DidVcsRepositoryChangeParams params) { notify(() -> getBean(SonarProjectBranchTrackingService.class).didVcsRepositoryChange(params.getConfigurationScopeId())); } @Override public CompletableFuture getMatchedSonarProjectBranch(GetMatchedSonarProjectBranchParams params) { return requestAsync( cancelMonitor -> new GetMatchedSonarProjectBranchResponse( getBean(SonarProjectBranchTrackingService.class).awaitEffectiveSonarProjectBranch(params.getConfigurationScopeId()).orElse(null))); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TaintVulnerabilityTrackingRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ListAllParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ListAllResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityTrackingRpcService; import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService; public class TaintVulnerabilityTrackingRpcServiceDelegate extends AbstractRpcServiceDelegate implements TaintVulnerabilityTrackingRpcService { public TaintVulnerabilityTrackingRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); } @Override public CompletableFuture listAll(ListAllParams params) { return requestAsync(cancelMonitor -> new ListAllResponse(getBean(TaintVulnerabilityTrackingService.class) .listAll(params.getConfigurationScopeId(), params.shouldRefresh(), cancelMonitor))); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TaskProgressRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import org.sonarsource.sonarlint.core.commons.progress.TaskManager; import org.sonarsource.sonarlint.core.rpc.protocol.backend.progress.CancelTaskParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.progress.TaskProgressRpcService; public class TaskProgressRpcServiceDelegate extends AbstractRpcServiceDelegate implements TaskProgressRpcService { public TaskProgressRpcServiceDelegate(SonarLintRpcServerImpl sonarLintRpcServer) { super(sonarLintRpcServer); } @Override public void cancelTask(CancelTaskParams params) { notify(() -> getBean(TaskManager.class).cancel(params.getTaskId())); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TelemetryRpcServiceDelegate.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.GetStatusResponse; import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AcceptedBindingSuggestionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AddQuickFixAppliedForRuleParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AddReportedRulesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisDoneOnSingleLanguageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingTriggeredParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.DevNotificationsClickedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FindingsFilteredParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionResolvedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.IdeLabsExternalLinkClickedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.IdeLabsFeedbackLinkClickedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.HelpAndFeedbackClickedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportModeUsedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.ToolCalledParams; import org.sonarsource.sonarlint.core.telemetry.TelemetryService; class TelemetryRpcServiceDelegate extends AbstractRpcServiceDelegate implements TelemetryRpcService { public TelemetryRpcServiceDelegate(SonarLintRpcServerImpl server) { super(server); } @Override public CompletableFuture getStatus() { return requestAsync(cancelMonitor -> getBean(TelemetryService.class).getStatus()); } @Override public void enableTelemetry() { notify(() -> getBean(TelemetryService.class).enableTelemetry()); } @Override public void disableTelemetry() { notify(() -> getBean(TelemetryService.class).disableTelemetry()); } @Override public void analysisDoneOnSingleLanguage(AnalysisDoneOnSingleLanguageParams params) { notify(() -> getBean(TelemetryService.class).analysisDoneOnSingleLanguage(params.getLanguage(), params.getAnalysisTimeMs())); } @Override public void analysisDoneOnMultipleFiles() { notify(() -> getBean(TelemetryService.class).analysisDoneOnMultipleFiles()); } @Override public void devNotificationsClicked(DevNotificationsClickedParams params) { notify(() -> getBean(TelemetryService.class).smartNotificationsClicked(params.getEventType())); } @Override public void taintVulnerabilitiesInvestigatedLocally() { notify(() -> getBean(TelemetryService.class).taintVulnerabilitiesInvestigatedLocally()); } @Override public void taintVulnerabilitiesInvestigatedRemotely() { notify(() -> getBean(TelemetryService.class).taintVulnerabilitiesInvestigatedRemotely()); } @Override public void addReportedRules(AddReportedRulesParams params) { notify(() -> getBean(TelemetryService.class).addReportedRules(params.getRuleKeys())); } @Override public void addQuickFixAppliedForRule(AddQuickFixAppliedForRuleParams params) { notify(() -> getBean(TelemetryService.class).addQuickFixAppliedForRule(params.getRuleKey())); } @Override public void helpAndFeedbackLinkClicked(HelpAndFeedbackClickedParams params) { notify(() -> getBean(TelemetryService.class).helpAndFeedbackLinkClicked(params)); } @Override public void mcpIntegrationEnabled() { notify(() -> getBean(TelemetryService.class).mcpIntegrationEnabled()); } @Override public void mcpTransportModeUsed(McpTransportModeUsedParams params) { notify(() -> getBean(TelemetryService.class).mcpTransportModeUsed(params.getMcpTransportMode())); } @Override public void toolCalled(ToolCalledParams params) { notify(() -> getBean(TelemetryService.class).toolCalled(params)); } @Override public void analysisReportingTriggered(AnalysisReportingTriggeredParams params) { notify(() -> getBean(TelemetryService.class).analysisReportingTriggered(params)); } @Override public void fixSuggestionResolved(FixSuggestionResolvedParams params) { notify(() -> getBean(TelemetryService.class).fixSuggestionResolved(params)); } @Override public void addedManualBindings() { notify(() -> getBean(TelemetryService.class).addedManualBindings()); } @Override public void acceptedBindingSuggestion(AcceptedBindingSuggestionParams params) { notify(() -> getBean(TelemetryService.class).acceptedBindingSuggestion(params.getOrigin())); } @Override public void addedImportedBindings() { notify(() -> getBean(TelemetryService.class).addedImportedBindings()); } @Override public void addedAutomaticBindings() { notify(() -> getBean(TelemetryService.class).addedAutomaticBindings()); } @Override public void taintInvestigatedLocally() { notify(() -> getBean(TelemetryService.class).taintInvestigatedLocally()); } @Override public void taintInvestigatedRemotely() { notify(() -> getBean(TelemetryService.class).taintInvestigatedRemotely()); } @Override public void hotspotInvestigatedLocally() { notify(() -> getBean(TelemetryService.class).hotspotInvestigatedLocally()); } @Override public void hotspotInvestigatedRemotely() { notify(() -> getBean(TelemetryService.class).hotspotInvestigatedRemotely()); } @Override public void issueInvestigatedLocally() { notify(() -> getBean(TelemetryService.class).issueInvestigatedLocally()); } @Override public void dependencyRiskInvestigatedLocally() { notify(() -> getBean(TelemetryService.class).dependencyRiskInvestigatedLocally()); } @Override public void findingsFiltered(FindingsFilteredParams params) { notify(() -> getBean(TelemetryService.class).findingsFiltered(params.getFilterType())); } @Override public void ideLabsExternalLinkClicked(IdeLabsExternalLinkClickedParams params) { notify(() -> getBean(TelemetryService.class).ideLabsLinkClicked(params.getLinkId())); } @Override public void ideLabsFeedbackLinkClicked(IdeLabsFeedbackLinkClickedParams params) { notify(() -> getBean(TelemetryService.class).ideLabsFeedbackLinkClicked(params.getFeatureId())); } @Override public void supportedLanguagesPanelOpened() { notify(() -> getBean(TelemetryService.class).supportedLanguagesPanelOpened()); } @Override public void supportedLanguagesPanelCtaClicked() { notify(() -> getBean(TelemetryService.class).supportedLanguagesPanelCtaClicked()); } } ================================================ FILE: backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/package-info.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.rpc.impl; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/rpc-impl/src/main/resources/logback.xml ================================================ ================================================ FILE: backend/rpc-impl/src/test/java/org/sonarsource/sonarlint/core/rpc/impl/AnalysisServiceTests.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.sonar.api.batch.sensor.issue.IssueLocation; import org.sonarsource.sonarlint.core.active.rules.ActiveRuleDetails; import org.sonarsource.sonarlint.core.analysis.RawIssue; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile; import org.sonarsource.sonarlint.core.analysis.api.ClientInputFileEdit; import org.sonarsource.sonarlint.core.analysis.api.Flow; import org.sonarsource.sonarlint.core.analysis.api.Issue; import org.sonarsource.sonarlint.core.analysis.api.QuickFix; import org.sonarsource.sonarlint.core.analysis.api.TextEdit; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextPointer; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.DefaultTextRange; import org.sonarsource.sonarlint.core.analysis.container.analysis.filesystem.SonarLintInputFile; import org.sonarsource.sonarlint.core.commons.api.TextRange; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class AnalysisServiceTests { @Test void it_should_convert_issue_flaws_and_quick_fixes_to_raw_issue_dto() throws IOException { var issueLocation = mock(IssueLocation.class); var inputComponent = mock(SonarLintInputFile.class); when(inputComponent.isFile()).thenReturn(true); when(inputComponent.key()).thenReturn("inputComponentKey"); when(issueLocation.message()).thenReturn("issue location message"); when(issueLocation.textRange()).thenReturn(new DefaultTextRange(new DefaultTextPointer(1, 2), new DefaultTextPointer(3, 4))); when(issueLocation.inputComponent()).thenReturn(inputComponent); var clientInputFile = mock(ClientInputFile.class); when(clientInputFile.contents()).thenReturn("content"); var issue = new Issue(new ActiveRuleDetails("repo:ruleKey", "languageKey", null, null, org.sonarsource.sonarlint.core.commons.IssueSeverity.BLOCKER, org.sonarsource.sonarlint.core.commons.RuleType.BUG, org.sonarsource.sonarlint.core.commons.CleanCodeAttribute.CLEAR, Map.of(), org.sonarsource.sonarlint.core.commons.VulnerabilityProbability.HIGH), "primary message", Map.of(), new DefaultTextRange(new DefaultTextPointer(1, 1), new DefaultTextPointer(1, 1)), clientInputFile, List.of(new Flow(List.of(issueLocation))), List.of(new QuickFix(List.of( new ClientInputFileEdit(clientInputFile, List.of(new TextEdit( new TextRange(5, 6, 7, 8), "Quick fix text")))), "Quick fix message")), Optional.of("")); var rawIssueDto = AnalysisRpcServiceDelegate.toDto(new RawIssue(issue)); assertThat(rawIssueDto.getRuleKey()).isEqualTo("repo:ruleKey"); var rawIssueLocationDto = rawIssueDto.getFlows().get(0).getLocations().get(0); assertThat(rawIssueLocationDto.getMessage()).isEqualTo("issue location message"); var issueLocationTextRange = rawIssueLocationDto.getTextRange(); assertThat(issueLocationTextRange).isNotNull(); assertThat(issueLocationTextRange.getStartLine()).isEqualTo(1); assertThat(issueLocationTextRange.getStartLineOffset()).isEqualTo(2); assertThat(issueLocationTextRange.getEndLine()).isEqualTo(3); assertThat(issueLocationTextRange.getEndLineOffset()).isEqualTo(4); var quickFix = rawIssueDto.getQuickFixes().get(0); assertThat(quickFix).isNotNull(); var fileEdit = quickFix.fileEdits().get(0); assertThat(fileEdit).isNotNull(); var textEdit = fileEdit.textEdits().get(0); assertThat(textEdit).isNotNull(); var textRange = textEdit.range(); assertThat(textRange).isNotNull(); assertThat(textRange.getStartLine()).isEqualTo(5); assertThat(textRange.getStartLineOffset()).isEqualTo(6); assertThat(textRange.getEndLine()).isEqualTo(7); assertThat(textRange.getEndLineOffset()).isEqualTo(8); } } ================================================ FILE: backend/rpc-impl/src/test/java/org/sonarsource/sonarlint/core/rpc/impl/SonarLintRpcServerImplTests.java ================================================ /* * SonarLint Core - RPC Implementation * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.time.Duration; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class SonarLintRpcServerImplTests { @Test void it_should_fail_to_use_services_if_the_backend_is_not_initialized() { var in = new ByteArrayInputStream(new byte[0]); var out = new ByteArrayOutputStream(); var backend = new SonarLintRpcServerImpl(in, out); assertThat(backend.getTelemetryService().getStatus()) .failsWithin(1, TimeUnit.MINUTES) .withThrowableOfType(ExecutionException.class) .withCauseInstanceOf(IllegalStateException.class) .withStackTraceContaining("Backend is not initialized"); } @Test void it_should_silently_shutdown_the_backend_if_it_was_not_initialized() { var in = new ByteArrayInputStream(new byte[0]); var out = new ByteArrayOutputStream(); var backend = new SonarLintRpcServerImpl(in, out); var future = backend.shutdown(); assertThat(future) .succeedsWithin(Duration.ofSeconds(1)); } } ================================================ FILE: backend/rule-extractor/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-rule-extractor SonarLint Core - Rule Extractor Extract rules metadata from plugins com.google.code.findbugs jsr305 provided ${project.groupId} sonarlint-plugin-commons ${project.version} ${project.groupId} sonarlint-commons ${project.version} org.sonarsource.sonarqube sonar-markdown ${sonar-markdown.version} org.slf4j slf4j-api org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test ch.qos.logback logback-classic test org.apache.maven.plugins maven-dependency-plugin copy-open-source-plugins-for-mediumtests generate-test-resources copy org.sonarsource.java sonar-java-plugin 8.25.0.42802 jar org.sonarsource.javascript sonar-javascript-plugin 11.8.0.37897 jar org.sonarsource.php sonar-php-plugin 3.55.0.15704 jar org.sonarsource.python sonar-python-plugin 5.18.0.31561 jar org.sonarsource.kotlin sonar-kotlin-plugin 3.4.0.8957 jar org.sonarsource.slang sonar-ruby-plugin 1.22.0.1992 jar org.sonarsource.slang sonar-scala-plugin 1.21.0.1997 jar org.sonarsource.html sonar-html-plugin 3.24.0.7341 jar org.sonarsource.xml sonar-xml-plugin 2.16.0.7616 jar ${project.build.directory}/plugins false true false commercial commercial org.apache.maven.plugins maven-dependency-plugin copy-commercial-plugins-for-mediumtests generate-test-resources copy com.sonarsource.abap sonar-abap-plugin 3.17.0.7722 jar com.sonarsource.cpp sonar-cfamily-plugin 6.78.0.96395 jar com.sonarsource.cobol sonar-cobol-plugin 5.11.0.10062 jar com.sonarsource.pli sonar-pli-plugin 1.21.0.7212 jar com.sonarsource.plsql sonar-plsql-plugin 3.18.1.230 jar com.sonarsource.rpg sonar-rpg-plugin 3.13.0.7515 jar com.sonarsource.swift sonar-swift-plugin 5.1.0.12421 jar com.sonarsource.slang sonar-apex-plugin 1.24.0.2040 jar com.sonarsource.tsql sonar-tsql-plugin 1.16.1.9133 jar ${project.build.directory}/plugins false true false conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/EmptyConfiguration.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Optional; import org.sonar.api.config.Configuration; public class EmptyConfiguration implements Configuration { @Override public Optional get(String key) { return Optional.empty(); } @Override public boolean hasKey(String key) { return false; } @Override public String[] getStringArray(String key) { return new String[0]; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/LegacyHotspotRuleDescriptionSectionsGenerator.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDescriptionSection.Context; import static org.apache.commons.lang3.StringUtils.trimToNull; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY; /** * @see Original on SonarQube */ public class LegacyHotspotRuleDescriptionSectionsGenerator { private LegacyHotspotRuleDescriptionSectionsGenerator() { // Static stuff only } static List extractDescriptionSectionsFromHtml(@Nullable String descriptionInHtml) { if (descriptionInHtml == null || descriptionInHtml.isEmpty()) { return List.of(); } String[] split = extractSection("", descriptionInHtml); String remainingText = split[0]; String ruleDescriptionSection = split[1]; split = extractSection("

Exceptions

", remainingText); remainingText = split[0]; String exceptions = split[1]; split = extractSection("

Ask Yourself Whether

", remainingText); remainingText = split[0]; String askSection = split[1]; split = extractSection("

Sensitive Code Example

", remainingText); remainingText = split[0]; String sensitiveSection = split[1]; split = extractSection("

Noncompliant Code Example

", remainingText); remainingText = split[0]; String noncompliantSection = split[1]; split = extractSection("

Recommended Secure Coding Practices

", remainingText); remainingText = split[0]; String recommendedSection = split[1]; split = extractSection("

Compliant Solution

", remainingText); remainingText = split[0]; String compliantSection = split[1]; split = extractSection("

See

", remainingText); remainingText = split[0]; String seeSection = split[1]; Optional rootSection = createSection(ROOT_CAUSE_SECTION_KEY, ruleDescriptionSection, exceptions, remainingText); Optional assessSection = createSection(ASSESS_THE_PROBLEM_SECTION_KEY, askSection, sensitiveSection, noncompliantSection); Optional fixSection = createSection(HOW_TO_FIX_SECTION_KEY, recommendedSection, compliantSection, seeSection); return Stream.of(rootSection, assessSection, fixSection) .filter(Predicate.not(Optional::isEmpty)) .flatMap(Optional::stream) .toList(); } private static String[] extractSection(String beginning, String description) { var endSection = "

"; var beginningIndex = description.indexOf(beginning); if (beginningIndex != -1) { var endIndex = description.indexOf(endSection, beginningIndex + beginning.length()); if (endIndex == -1) { endIndex = description.length(); } return new String[] { description.substring(0, beginningIndex) + description.substring(endIndex), description.substring(beginningIndex, endIndex) }; } else { return new String[] {description, ""}; } } private static Optional createSection(String sectionKey, String... contentPieces) { var content = trimToNull(String.join("", contentPieces)); if (content == null) { return Optional.empty(); } return Optional.of(new SonarLintRuleDescriptionSection(sectionKey, content, emptyContextForConvertedHotspotSection())); } private static Optional emptyContextForConvertedHotspotSection() { return Optional.empty(); } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/NoopTempFolder.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.io.File; import javax.annotation.Nullable; import org.sonar.api.utils.TempFolder; public class NoopTempFolder implements TempFolder { @Override public File newDir() { throw new UnsupportedOperationException("newDir"); } @Override public File newDir(String name) { throw new UnsupportedOperationException("newDir"); } @Override public File newFile() { throw new UnsupportedOperationException("newFile"); } @Override public File newFile(@Nullable String prefix, @Nullable String suffix) { throw new UnsupportedOperationException("newFile"); } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/RuleDefinitionsLoader.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.List; import java.util.Optional; import org.sonar.api.server.rule.RulesDefinition; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; /** * Load rules directly from plugins {@link RulesDefinition} */ public class RuleDefinitionsLoader { private final RulesDefinition.Context context; public RuleDefinitionsLoader(Optional> pluginDefs) { context = new RulesDefinition.Context(); for (var pluginDefinition : pluginDefs.orElse(List.of())) { try { pluginDefinition.define(context); } catch (Exception e) { SonarLintLogger.get().warn(String.format("Failed to load rule definitions for %s, associated rules will be skipped", pluginDefinition), e); } } } public RulesDefinition.Context getContext() { return context; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/RuleExtractionSettings.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import org.sonar.api.config.PropertyDefinitions; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.MapSettings; public class RuleExtractionSettings extends MapSettings { public RuleExtractionSettings(PropertyDefinitions definitions, RuleSettings settings) { super(definitions, settings.settings()); } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/RuleSettings.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Map; public record RuleSettings(Map settings) { } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/RulesDefinitionExtractor.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import org.sonar.api.Plugin; import org.sonar.api.rules.RuleType; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinition.Context; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class RulesDefinitionExtractor { public List extractRules(Map pluginInstancesByKeys, Set enabledLanguages, boolean includeTemplateRules, boolean includeSecurityHotspots, RuleSettings settings) { Context context; try { var container = new RulesDefinitionExtractorContainer(pluginInstancesByKeys, settings); container.execute(null); context = container.getRulesDefinitionContext(); } catch (Exception e) { throw new IllegalStateException("Unable to extract rules metadata", e); } List rules = new ArrayList<>(); for (var repoDef : context.repositories()) { if (repoDef.isExternal()) { continue; } var repoLanguage = SonarLanguage.forKey(repoDef.language()); if (repoLanguage.isEmpty() || !enabledLanguages.contains(repoLanguage.get())) { continue; } for (RulesDefinition.Rule ruleDef : repoDef.rules()) { if (shouldIgnoreAsHotspot(includeSecurityHotspots, ruleDef) || shouldIgnoreAsTemplate(includeTemplateRules, ruleDef)) { continue; } rules.add(new SonarLintRuleDefinition(ruleDef)); } } return rules; } private static boolean shouldIgnoreAsTemplate(boolean includeTemplateRules, RulesDefinition.Rule ruleDef) { return ruleDef.template() && !includeTemplateRules; } private static boolean shouldIgnoreAsHotspot(boolean hotspotsEnabled, RulesDefinition.Rule ruleDef) { return ruleDef.type() == RuleType.SECURITY_HOTSPOT && !hotspotsEnabled; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/RulesDefinitionExtractorContainer.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Map; import org.sonar.api.Plugin; import org.sonar.api.SonarQubeVersion; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.server.rule.RulesDefinition.Context; import org.sonar.api.server.rule.RulesDefinitionXmlLoader; import org.sonar.api.utils.AnnotationUtils; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.core.plugin.commons.ApiVersions; import org.sonarsource.sonarlint.core.plugin.commons.ExtensionInstaller; import org.sonarsource.sonarlint.core.plugin.commons.ExtensionUtils; import org.sonarsource.sonarlint.core.plugin.commons.container.SpringComponentContainer; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.ConfigurationBridge; import org.sonarsource.sonarlint.core.plugin.commons.sonarapi.SonarLintRuntimeImpl; public class RulesDefinitionExtractorContainer extends SpringComponentContainer { private Context rulesDefinitionContext; private final Map pluginInstancesByKeys; private final RuleSettings settings; public RulesDefinitionExtractorContainer(Map pluginInstancesByKeys, RuleSettings settings) { this.pluginInstancesByKeys = pluginInstancesByKeys; this.settings = settings; } @Override protected void doBeforeStart() { var sonarPluginApiVersion = ApiVersions.loadSonarPluginApiVersion(); var sonarlintPluginApiVersion = ApiVersions.loadSonarLintPluginApiVersion(); var sonarLintRuntime = new SonarLintRuntimeImpl(sonarPluginApiVersion, sonarlintPluginApiVersion, -1); var extensionInstaller = new ExtensionInstaller(sonarLintRuntime, new EmptyConfiguration()); extensionInstaller.install(this, pluginInstancesByKeys, (key, ext) -> { if (ExtensionUtils.isType(ext, Sensor.class)) { // Optimization, and allows to run with the Xoo plugin return false; } var annotation = AnnotationUtils.getAnnotation(ext, SonarLintSide.class); if (annotation != null) { var lifespan = annotation.lifespan(); return SonarLintSide.SINGLE_ANALYSIS.equals(lifespan); } return false; }); add( settings, ConfigurationBridge.class, RuleExtractionSettings.class, sonarLintRuntime, new SonarQubeVersion(sonarPluginApiVersion), RulesDefinitionXmlLoader.class, RuleDefinitionsLoader.class, NoopTempFolder.class); } @Override protected void doAfterStart() { this.rulesDefinitionContext = getComponentByType(RuleDefinitionsLoader.class).getContext(); } public Context getRulesDefinitionContext() { return rulesDefinitionContext; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/SecurityStandards.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.annotation.concurrent.Immutable; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.commons.VulnerabilityProbability.HIGH; import static org.sonarsource.sonarlint.core.commons.VulnerabilityProbability.LOW; import static org.sonarsource.sonarlint.core.commons.VulnerabilityProbability.MEDIUM; @Immutable public final class SecurityStandards { public static final String UNKNOWN_STANDARD = "unknown"; private static final String CWE_PREFIX = "cwe:"; public enum SLCategory { BUFFER_OVERFLOW("buffer-overflow", HIGH), SQL_INJECTION("sql-injection", HIGH), RCE("rce", MEDIUM), OBJECT_INJECTION("object-injection", LOW), COMMAND_INJECTION("command-injection", HIGH), PATH_TRAVERSAL_INJECTION("path-traversal-injection", HIGH), LDAP_INJECTION("ldap-injection", LOW), XPATH_INJECTION("xpath-injection", LOW), LOG_INJECTION("log-injection", LOW), XXE("xxe", MEDIUM), XSS("xss", HIGH), DOS("dos", MEDIUM), SSRF("ssrf", MEDIUM), CSRF("csrf", HIGH), HTTP_RESPONSE_SPLITTING("http-response-splitting", LOW), OPEN_REDIRECT("open-redirect", MEDIUM), WEAK_CRYPTOGRAPHY("weak-cryptography", MEDIUM), AUTH("auth", HIGH), INSECURE_CONF("insecure-conf", LOW), FILE_MANIPULATION("file-manipulation", LOW), ENCRYPTION_OF_SENSITIVE_DATA("encrypt-data", LOW), TRACEABILITY("traceability", LOW), PERMISSION("permission", MEDIUM), OTHERS("others", LOW); private final String key; private final VulnerabilityProbability vulnerability; SLCategory(String key, VulnerabilityProbability vulnerability) { this.key = key; this.vulnerability = vulnerability; } public String getKey() { return key; } public VulnerabilityProbability getVulnerability() { return vulnerability; } } public static final Map> CWES_BY_SL_CATEGORY = Map.ofEntries( Map.entry(SLCategory.BUFFER_OVERFLOW, Set.of("119", "120", "131", "676", "788")), Map.entry(SLCategory.SQL_INJECTION, Set.of("89", "564", "943")), Map.entry(SLCategory.COMMAND_INJECTION, Set.of("77", "78", "88", "214")), Map.entry(SLCategory.PATH_TRAVERSAL_INJECTION, Set.of("22")), Map.entry(SLCategory.LDAP_INJECTION, Set.of("90")), Map.entry(SLCategory.XPATH_INJECTION, Set.of("643")), Map.entry(SLCategory.RCE, Set.of("94", "95")), Map.entry(SLCategory.DOS, Set.of("400", "624")), Map.entry(SLCategory.SSRF, Set.of("918")), Map.entry(SLCategory.CSRF, Set.of("352")), Map.entry(SLCategory.XSS, Set.of("79", "80", "81", "82", "83", "84", "85", "86", "87")), Map.entry(SLCategory.LOG_INJECTION, Set.of("117")), Map.entry(SLCategory.HTTP_RESPONSE_SPLITTING, Set.of("113")), Map.entry(SLCategory.OPEN_REDIRECT, Set.of("601")), Map.entry(SLCategory.XXE, Set.of("611", "827")), Map.entry(SLCategory.OBJECT_INJECTION, Set.of("134", "470", "502")), Map.entry(SLCategory.WEAK_CRYPTOGRAPHY, Set.of("295", "297", "321", "322", "323", "324", "325", "326", "327", "328", "330", "780")), Map.entry(SLCategory.AUTH, Set.of("798", "640", "620", "549", "522", "521", "263", "262", "261", "259", "308")), Map.entry(SLCategory.INSECURE_CONF, Set.of("102", "215", "346", "614", "489", "942")), Map.entry(SLCategory.FILE_MANIPULATION, Set.of("97", "73")), Map.entry(SLCategory.ENCRYPTION_OF_SENSITIVE_DATA, Set.of("311", "315", "319")), Map.entry(SLCategory.TRACEABILITY, Set.of("778")), Map.entry(SLCategory.PERMISSION, Set.of("266", "269", "284", "668", "732"))); private final Set standards; private final Set cwe; private final SLCategory sLCategory; private final Set ignoredSLCategories; private SecurityStandards(Set standards, Set cwe, SLCategory sLCategory, Set ignoredSLCategories) { this.standards = standards; this.cwe = cwe; this.sLCategory = sLCategory; this.ignoredSLCategories = ignoredSLCategories; } public SLCategory getSlCategory() { return sLCategory; } /** * If CWEs mapped to multiple {@link SLCategory}, those which are not taken into account are listed here. */ public Set getIgnoredSLCategories() { return ignoredSLCategories; } public Set getStandards() { return standards; } public Set getCwe() { return cwe; } /** * @throws IllegalStateException if {@code securityStandards} maps to multiple {@link SLCategory SLCategories} */ public static SecurityStandards fromSecurityStandards(Set securityStandards) { Set standards = securityStandards.stream().filter(Objects::nonNull).collect(toSet()); Set cwe = toCwes(standards); List sl = toSLCategories(cwe); var slCategory = sl.iterator().next(); Set ignoredSLCategories = sl.stream().skip(1).collect(toSet()); return new SecurityStandards(standards, cwe, slCategory, ignoredSLCategories); } private static Set toCwes(Collection securityStandards) { Set result = securityStandards.stream() .filter(s -> s.startsWith(CWE_PREFIX)) .map(s -> s.substring(CWE_PREFIX.length())) .collect(toSet()); return result.isEmpty() ? singleton(UNKNOWN_STANDARD) : result; } private static List toSLCategories(Collection cwe) { List result = CWES_BY_SL_CATEGORY .keySet() .stream() .filter(k -> cwe.stream().anyMatch(CWES_BY_SL_CATEGORY.get(k)::contains)) .toList(); return result.isEmpty() ? singletonList(SLCategory.OTHERS) : result; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/SonarLintRuleDefinition.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.sonar.api.rule.RuleKey; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinition.Param; import org.sonar.markdown.Markdown; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.rule.extractor.SecurityStandards.fromSecurityStandards; public class SonarLintRuleDefinition { private final String key; private final String name; private final IssueSeverity defaultSeverity; private final RuleType type; private final CleanCodeAttribute cleanCodeAttribute; private final Map defaultImpacts; private final String description; private final List descriptionSections; private final Map params; private final Map defaultParams = new HashMap<>(); private final boolean isActiveByDefault; private final SonarLanguage language; private final String[] tags; private final Set deprecatedKeys; private final Set educationPrincipleKeys; private final Optional internalKey; // Relevant for Hotspot rules only private final Optional vulnerabilityProbability; public SonarLintRuleDefinition(RulesDefinition.Rule rule) { this.key = RuleKey.of(rule.repository().key(), rule.key()).toString(); this.name = rule.name(); this.defaultSeverity = IssueSeverity.valueOf(rule.severity()); this.type = RuleType.valueOf(rule.type().name()); this.cleanCodeAttribute = Optional.ofNullable(rule.cleanCodeAttribute()).map(Enum::name).map(CleanCodeAttribute::valueOf) .orElse(CleanCodeAttribute.defaultCleanCodeAttribute()); this.defaultImpacts = rule.defaultImpacts().entrySet() .stream() .map(e -> Map.entry(SoftwareQuality.valueOf(e.getKey().name()), ImpactSeverity.valueOf(e.getValue().name()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); var htmlDescription = rule.htmlDescription() != null ? rule.htmlDescription() : Markdown.convertToHtml(rule.markdownDescription()); if (rule.type() == org.sonar.api.rules.RuleType.SECURITY_HOTSPOT) { this.description = null; this.descriptionSections = LegacyHotspotRuleDescriptionSectionsGenerator.extractDescriptionSectionsFromHtml(htmlDescription); } else { this.description = htmlDescription; this.descriptionSections = rule.ruleDescriptionSections().stream().map(s -> new SonarLintRuleDescriptionSection(s.getKey(), s.getHtmlContent(), s.getContext().map(c -> new SonarLintRuleDescriptionSection.Context(c.getKey(), c.getDisplayName())))).toList(); } this.isActiveByDefault = rule.activatedByDefault(); this.language = SonarLanguage.forKey(rule.repository().language()).orElseThrow(() -> new IllegalStateException("Unknown language with key: " + rule.repository().language())); this.tags = rule.tags().toArray(new String[0]); this.deprecatedKeys = rule.deprecatedRuleKeys().stream().map(RuleKey::toString).collect(toSet()); this.educationPrincipleKeys = rule.educationPrincipleKeys(); this.vulnerabilityProbability = rule.type() == org.sonar.api.rules.RuleType.SECURITY_HOTSPOT ? Optional.of(fromSecurityStandards(rule.securityStandards()).getSlCategory().getVulnerability()) : Optional.empty(); Map builder = new HashMap<>(); for (Param param : rule.params()) { var paramDefinition = new SonarLintRuleParamDefinition(param); builder.put(param.key(), paramDefinition); var defaultValue = paramDefinition.defaultValue(); if (defaultValue != null) { defaultParams.put(param.key(), defaultValue); } } params = Collections.unmodifiableMap(builder); this.internalKey = Optional.ofNullable(rule.internalKey()); } public String getKey() { return key; } public String getName() { return name; } public IssueSeverity getDefaultSeverity() { return defaultSeverity; } public RuleType getType() { return type; } public Optional getCleanCodeAttribute() { return Optional.ofNullable(cleanCodeAttribute); } public Map getDefaultImpacts() { return defaultImpacts; } public Map getParams() { return params; } public Map getDefaultParams() { return defaultParams; } public boolean isActiveByDefault() { return isActiveByDefault; } public String getHtmlDescription() { return description; } public List getDescriptionSections() { return descriptionSections; } public SonarLanguage getLanguage() { return language; } public String[] getTags() { return tags; } public Set getDeprecatedKeys() { return deprecatedKeys; } public Set getEducationPrincipleKeys() { return educationPrincipleKeys; } public Optional getInternalKey() { return internalKey; } public Optional getVulnerabilityProbability() { return vulnerabilityProbability; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/SonarLintRuleDescriptionSection.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Optional; public class SonarLintRuleDescriptionSection { private final String key; private final String htmlContent; private final Optional context; public SonarLintRuleDescriptionSection(String key, String htmlContent, Optional context) { this.key = key; this.htmlContent = htmlContent; this.context = context; } public String getKey() { return key; } public String getHtmlContent() { return htmlContent; } public Optional getContext() { return context; } public static class Context { private final String key; private final String displayName; public Context(String key, String displayName) { this.key = key; this.displayName = displayName; } public String getKey() { return key; } public String getDisplayName() { return displayName; } } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/SonarLintRuleParamDefinition.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Collections; import java.util.List; import javax.annotation.CheckForNull; import org.sonar.api.server.rule.RuleParamType; import org.sonar.api.server.rule.RulesDefinition.Param; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; public class SonarLintRuleParamDefinition { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final String key; private final String name; private final String description; private final String defaultValue; private final SonarLintRuleParamType type; private final boolean multiple; private final List possibleValues; public SonarLintRuleParamDefinition(Param param) { this.key = param.key(); this.name = param.name(); this.description = param.description(); this.defaultValue = param.defaultValue(); var apiType = param.type(); this.type = from(apiType); this.multiple = apiType.multiple(); this.possibleValues = Collections.unmodifiableList(apiType.values()); } private static SonarLintRuleParamType from(RuleParamType apiType) { try { return SonarLintRuleParamType.valueOf(apiType.type()); } catch (IllegalArgumentException unknownType) { LOG.warn("Unknown parameter type: " + apiType.type()); return SonarLintRuleParamType.STRING; } } public String key() { return key; } public String name() { return name; } public String description() { return description; } @CheckForNull public String defaultValue() { return defaultValue; } public SonarLintRuleParamType type() { return type; } public boolean multiple() { return multiple; } public List possibleValues() { return possibleValues; } } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/SonarLintRuleParamType.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; public enum SonarLintRuleParamType { /** * Keep in sync with constants in org.sonar.api.server.rule.RuleParamType */ STRING, TEXT, BOOLEAN, INTEGER, FLOAT } ================================================ FILE: backend/rule-extractor/src/main/java/org/sonarsource/sonarlint/core/rule/extractor/package-info.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.rule.extractor; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/rule-extractor/src/test/java/mediumtests/RuleExtractorMediumTests.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package mediumtests; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; import java.util.Map; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.plugin.commons.PluginsLoader; import org.sonarsource.sonarlint.core.rule.extractor.RuleSettings; import org.sonarsource.sonarlint.core.rule.extractor.RulesDefinitionExtractor; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition; import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleParamType; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class RuleExtractorMediumTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final int COMMERCIAL_RULE_TEMPLATES_COUNT = 11; private static final int NON_COMMERCIAL_RULE_TEMPLATES_COUNT = 16; private static final int COMMERCIAL_SECURITY_HOTSPOTS_COUNT = 82; private static final int NON_COMMERCIAL_SECURITY_HOTSPOTS_COUNT = 298; private static final int ALL_RULES_COUNT_WITHOUT_COMMERCIAL = 2732; private static final int ALL_RULES_COUNT_WITH_COMMERCIAL = 4823; // commercial plugins might not be available // (if you pass -Dcommercial to maven, a profile will be activated that downloads the commercial plugins) private static final boolean COMMERCIAL_ENABLED = System.getProperty("commercial") != null; private static final Optional NODE_VERSION = Optional.of(Version.create("20.20.0")); private static final RuleSettings EMPTY_SETTINGS = new RuleSettings(Map.of()); private static Set allJars; @BeforeAll static void prepare() throws IOException { var dir = Paths.get("target/plugins/"); try (var files = Files.list(dir)) { allJars = files.filter(x -> x.getFileName().toString().endsWith(".jar")).collect(toSet()); } } @Test void extractAllRules() { var enabledLanguages = Set.of(SonarLanguage.values()); var config = new PluginsLoader.Configuration(allJars, enabledLanguages, false, NODE_VERSION); var result = new PluginsLoader().load(config, Set.of()); var allRules = new RulesDefinitionExtractor().extractRules(result.getLoadedPlugins().getAllPluginInstancesByKeys(), enabledLanguages, false, false, EMPTY_SETTINGS); if (COMMERCIAL_ENABLED) { assertThat(allJars).hasSize(18); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITH_COMMERCIAL); } else { assertThat(allJars).hasSize(9); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITHOUT_COMMERCIAL); } var pythonRule = allRules.stream().filter(r -> r.getKey().equals("python:S139")).findFirst(); assertThat(pythonRule).hasValueSatisfying(rule -> { assertThat(rule.getKey()).isEqualTo("python:S139"); assertThat(rule.getType()).isEqualTo(RuleType.CODE_SMELL); assertThat(rule.getDefaultSeverity()).isEqualTo(IssueSeverity.MINOR); assertThat(rule.getLanguage()).isEqualTo(SonarLanguage.PYTHON); assertThat(rule.getName()).isEqualTo("Comments should not be located at the end of lines of code"); assertThat(rule.isActiveByDefault()).isFalse(); assertThat(rule.getParams()) .hasSize(1) .hasEntrySatisfying("legalTrailingCommentPattern", param -> { assertThat(param.defaultValue()).isEqualTo("^#\\s*+([^\\s]++|fmt.*|type.*|noqa.*)$"); assertThat(param.description()) .isEqualTo("Pattern for text of trailing comments that are allowed. By default, Mypy and Black pragma comments as well as comments containing only one word."); assertThat(param.key()).isEqualTo("legalTrailingCommentPattern"); assertThat(param.multiple()).isFalse(); assertThat(param.name()).isEqualTo("legalTrailingCommentPattern"); assertThat(param.possibleValues()).isEmpty(); assertThat(param.type()).isEqualTo(SonarLintRuleParamType.STRING); }); assertThat(rule.getDefaultParams()).containsOnly(entry("legalTrailingCommentPattern", "^#\\s*+([^\\s]++|fmt.*|type.*|noqa.*)$")); assertThat(rule.getDeprecatedKeys()).isEmpty(); assertThat(rule.getHtmlDescription()).contains("

This rule verifies that single-line comments are not located"); assertThat(rule.getTags()).containsOnly("convention"); assertThat(rule.getInternalKey()).isEmpty(); }); var ruleWithInternalKey = allRules.stream().filter(r -> r.getKey().equals("java:NoSonar")).findFirst(); assertThat(ruleWithInternalKey).isNotEmpty(); assertThat(ruleWithInternalKey.get().getInternalKey()).contains("S1291"); } @Test void extractAllRules_include_rule_templates() { var enabledLanguages = Set.of(SonarLanguage.values()); var config = new PluginsLoader.Configuration(allJars, enabledLanguages, false, NODE_VERSION); var result = new PluginsLoader().load(config, Set.of()); var allRules = new RulesDefinitionExtractor().extractRules(result.getLoadedPlugins().getAllPluginInstancesByKeys(), enabledLanguages, true, false, EMPTY_SETTINGS); if (COMMERCIAL_ENABLED) { assertThat(allJars).hasSize(18); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITH_COMMERCIAL + NON_COMMERCIAL_RULE_TEMPLATES_COUNT + COMMERCIAL_RULE_TEMPLATES_COUNT); } else { assertThat(allJars).hasSize(9); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITHOUT_COMMERCIAL + NON_COMMERCIAL_RULE_TEMPLATES_COUNT); } } @Test void extractAllRules_include_security_hotspots() { var enabledLanguages = Set.of(SonarLanguage.values()); var config = new PluginsLoader.Configuration(allJars, enabledLanguages, false, NODE_VERSION); var result = new PluginsLoader().load(config, Set.of()); var allRules = new RulesDefinitionExtractor().extractRules(result.getLoadedPlugins().getAllPluginInstancesByKeys(), enabledLanguages, false, true, EMPTY_SETTINGS); if (COMMERCIAL_ENABLED) { assertThat(allJars).hasSize(18); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITH_COMMERCIAL + NON_COMMERCIAL_SECURITY_HOTSPOTS_COUNT + COMMERCIAL_SECURITY_HOTSPOTS_COUNT); } else { assertThat(allJars).hasSize(9); assertThat(allRules).hasSize(ALL_RULES_COUNT_WITHOUT_COMMERCIAL + NON_COMMERCIAL_SECURITY_HOTSPOTS_COUNT); } } @Test void onlyLoadRulesOfEnabledLanguages() { Set enabledLanguages = EnumSet.of( SonarLanguage.JAVA, // Enable JS but not TS SonarLanguage.JS, SonarLanguage.PHP, SonarLanguage.PYTHON); if (COMMERCIAL_ENABLED) { // Enable C but not C++ enabledLanguages.add(SonarLanguage.C); } var config = new PluginsLoader.Configuration(allJars, enabledLanguages, false, NODE_VERSION); var result = new PluginsLoader().load(config, Set.of()); var allRules = new RulesDefinitionExtractor().extractRules(result.getLoadedPlugins().getAllPluginInstancesByKeys(), enabledLanguages, false, false, EMPTY_SETTINGS); assertThat(allRules.stream().map(SonarLintRuleDefinition::getLanguage).distinct()).hasSameElementsAs(enabledLanguages); } @Test void loadNoRuleIfThereIsNoPlugin() { var enabledLanguages = Set.of(SonarLanguage.values()); var config = new PluginsLoader.Configuration(Set.of(), enabledLanguages, false, NODE_VERSION); var result = new PluginsLoader().load(config, Set.of()); var allRules = new RulesDefinitionExtractor().extractRules(result.getLoadedPlugins().getAllPluginInstancesByKeys(), enabledLanguages, false, false, EMPTY_SETTINGS); assertThat(allRules).isEmpty(); } } ================================================ FILE: backend/rule-extractor/src/test/java/org/sonarsource/sonarlint/core/rule/extractor/EmptyConfigurationTest.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class EmptyConfigurationTest { @Test void should_be_empty() { var emptyConfiguration = new EmptyConfiguration(); assertThat(emptyConfiguration.hasKey("")).isFalse(); assertThat(emptyConfiguration.get("")).isEmpty(); assertThat(emptyConfiguration.getStringArray("")).isEmpty(); } } ================================================ FILE: backend/rule-extractor/src/test/java/org/sonarsource/sonarlint/core/rule/extractor/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import java.util.Map; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY; import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY; import static org.sonarsource.sonarlint.core.rule.extractor.LegacyHotspotRuleDescriptionSectionsGenerator.extractDescriptionSectionsFromHtml; /** * @see SonarQube Test */ class LegacyHotspotRuleDescriptionSectionsGeneratorTest { /* * Bunch of static constant to create rule description. */ private static final String DESCRIPTION = """

The use of operators pairs ( =+, =- or =! ) where the reversed, single operator was meant (+=, -= or !=) will compile and run, but not produce the expected results.

This rule raises an issue when =+, =-, or =! is used without any spacing between the two operators and when there is at least one whitespace character after.

"""; private static final String NON_COMPLIANT_CODE = """

Noncompliant Code Example

Integer target = -5;
    Integer num = 3;
    
    target =- num;  // Noncompliant; target = -3. Is that really what's meant?
    target =+ num; // Noncompliant; target = 3
    
"""; private static final String COMPLIANT_CODE = """

Compliant Solution

Integer target = -5;
      Integer num = 3;
      
      target = -num;  // Compliant; intent to assign inverse value of num is clear
      target += num;
      
"""; private static final String SEE = """

See

"""; private static final String RECOMMENDED_CODING_PRACTICE = """

Recommended Secure Coding Practices

  • activate Spring Security's CSRF protection.
"""; private static final String ASK_AT_RISK = """

Ask Yourself Whether

  • Any URLs responding with Access-Control-Allow-Origin: * include sensitive content.
  • Any domains specified in Access-Control-Allow-Origin headers are checked against a whitelist.
"""; private static final String SENSITIVE_CODE = """

Sensitive Code Example

    // === Java Servlet ===
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      resp.setHeader("Content-Type", "text/plain; charset=utf-8");
      resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8080"); // Questionable
      resp.setHeader("Access-Control-Allow-Credentials", "true"); // Questionable
      resp.setHeader("Access-Control-Allow-Methods", "GET"); // Questionable
      resp.getWriter().write("response");
    }
    
    // === Spring MVC Controller annotation ===
    @CrossOrigin(origins = "http://domain1.com") // Questionable
    @RequestMapping("")
    public class TestController {
        public String home(ModelMap model) {
            model.addAttribute("message", "ok ");
            return "view";
        }
    
        @CrossOrigin(origins = "http://domain2.com") // Questionable
        @RequestMapping(value = "/test1")
        public ResponseEntity<String> test1() {
            return ResponseEntity.ok().body("ok");
        }
    }
    
"""; @Test void shouldReturnNoSectionForNullDescription() { assertThat(extractDescriptionSectionsFromHtml(null)).isEmpty(); } @Test void shouldReturnNoSectionForEmptyDescription() { assertThat(extractDescriptionSectionsFromHtml("")).isEmpty(); } @Test void parse_to_risk_description_fields_when_desc_contains_no_section() { var descriptionWithoutTitles = "description without titles"; assertThat(sectionsMapFromHtml(descriptionWithoutTitles)).hasSize(1) .containsEntry(ROOT_CAUSE_SECTION_KEY, descriptionWithoutTitles); } @Test void parse_return_null_risk_when_desc_starts_with_ask_yourself_title() { assertThat(sectionsMapFromHtml(ASK_AT_RISK + RECOMMENDED_CODING_PRACTICE)).hasSize(2) .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASK_AT_RISK) .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENDED_CODING_PRACTICE); } @Test void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() { assertThat(sectionsMapFromHtml(DESCRIPTION + RECOMMENDED_CODING_PRACTICE)).hasSize(2) .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION) .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENDED_CODING_PRACTICE); } @Test void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() { assertThat(sectionsMapFromHtml(DESCRIPTION + ASK_AT_RISK)).hasSize(2) .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION) .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASK_AT_RISK); } @Test void parse_with_noncompliant_section_not_removed() { assertThat(sectionsMapFromHtml(DESCRIPTION + NON_COMPLIANT_CODE + COMPLIANT_CODE)).hasSize(3) .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION) .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NON_COMPLIANT_CODE) .containsEntry(HOW_TO_FIX_SECTION_KEY, COMPLIANT_CODE); } @Test void parse_moved_noncompliant_code() { assertThat(sectionsMapFromHtml(DESCRIPTION + RECOMMENDED_CODING_PRACTICE + NON_COMPLIANT_CODE + SEE)).hasSize(3) .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION) .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NON_COMPLIANT_CODE) .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENDED_CODING_PRACTICE + SEE); } @Test void parse_moved_sensitivecode_code() { assertThat(sectionsMapFromHtml(DESCRIPTION + ASK_AT_RISK + RECOMMENDED_CODING_PRACTICE + SENSITIVE_CODE + SEE)).hasSize(3) .containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION) .containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASK_AT_RISK + SENSITIVE_CODE) .containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENDED_CODING_PRACTICE + SEE); } private static Map sectionsMapFromHtml(String html) { return extractDescriptionSectionsFromHtml(html).stream() .collect(Collectors.toMap(SonarLintRuleDescriptionSection::getKey, SonarLintRuleDescriptionSection::getHtmlContent)); } } ================================================ FILE: backend/rule-extractor/src/test/java/org/sonarsource/sonarlint/core/rule/extractor/NoopTempFolderTest.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; class NoopTempFolderTest { @Test void should_not_be_implemented() { var noopTempFolder = new NoopTempFolder(); assertThrows(UnsupportedOperationException.class, noopTempFolder::newDir); assertThrows(UnsupportedOperationException.class, noopTempFolder::newFile); assertThrows(UnsupportedOperationException.class, () -> noopTempFolder.newDir("name")); assertThrows(UnsupportedOperationException.class, () -> noopTempFolder.newFile("prefix", "suffix")); } } ================================================ FILE: backend/rule-extractor/src/test/java/org/sonarsource/sonarlint/core/rule/extractor/SecurityStandardsTest.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import org.junit.jupiter.api.Test; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.rule.extractor.SecurityStandards.CWES_BY_SL_CATEGORY; import static org.sonarsource.sonarlint.core.rule.extractor.SecurityStandards.fromSecurityStandards; class SecurityStandardsTest { @Test void fromSecurityStandards_from_empty_set_has_SLCategory_OTHERS() { SecurityStandards securityStandards = fromSecurityStandards(emptySet()); assertThat(securityStandards.getStandards()).isEmpty(); assertThat(securityStandards.getSlCategory()).isEqualTo(SecurityStandards.SLCategory.OTHERS); assertThat(securityStandards.getIgnoredSLCategories()).isEmpty(); } @Test void fromSecurityStandards_from_empty_set_has_unknown_cwe_standard() { SecurityStandards securityStandards = fromSecurityStandards(emptySet()); assertThat(securityStandards.getStandards()).isEmpty(); assertThat(securityStandards.getCwe()).containsOnly("unknown"); } @Test void fromSecurityStandards_finds_SLCategory_from_any_if_the_mapped_CWE_standard() { CWES_BY_SL_CATEGORY.forEach((slCategory, cwes) -> { cwes.forEach(cwe -> { SecurityStandards securityStandards = fromSecurityStandards(singleton("cwe:" + cwe)); assertThat(securityStandards.getSlCategory()).isEqualTo(slCategory); }); }); } @Test void fromSecurityStandards_finds_SLCategory_from_multiple_of_the_mapped_CWE_standard() { CWES_BY_SL_CATEGORY.forEach((slCategory, cwes) -> { SecurityStandards securityStandards = fromSecurityStandards(cwes.stream().map(t -> "cwe:" + t).collect(toSet())); assertThat(securityStandards.getSlCategory()).isEqualTo(slCategory); }); } } ================================================ FILE: backend/rule-extractor/src/test/java/org/sonarsource/sonarlint/core/rule/extractor/SonarLintRuleDefinitionTests.java ================================================ /* * SonarLint Core - Rule Extractor * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rule.extractor; import org.junit.jupiter.api.Test; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinition.NewRepository; import org.sonar.api.server.rule.RulesDefinition.NewRule; import org.sonar.api.server.rule.RulesDefinition.Rule; import static org.assertj.core.api.Assertions.assertThat; class SonarLintRuleDefinitionTests { @Test void convertMarkdownDescriptionToHtml() { RulesDefinition.Context context = new RulesDefinition.Context(); NewRepository newRepository = context.createRepository("my-repo", "java"); NewRule createRule = newRepository.createRule("my-rule-with-markdown-description") .setName("My Rule"); createRule.setMarkdownDescription(" = Title\n * one\n* two"); newRepository.done(); Rule rule = context.repositories().get(0).rule("my-rule-with-markdown-description"); SonarLintRuleDefinition underTest = new SonarLintRuleDefinition(rule); assertThat(underTest.getHtmlDescription()).isEqualTo("

Title

  • one
  • \n" + "
  • two
"); } } ================================================ FILE: backend/rule-extractor/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/server-api/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-server-api SonarLint Core - Server API Interaction with the server through its web API com.google.code.findbugs jsr305 provided org.apache.commons commons-lang3 com.google.guava guava ${project.groupId} sonarlint-commons ${project.version} ${project.groupId} sonarlint-http ${project.version} com.google.code.gson gson com.google.protobuf protobuf-java ${protobuf.version} org.sonarsource.sonarqube sonar-scanner-protocol ${sonar-scanner-protocol.version} * * com.squareup.okhttp3 okhttp test com.squareup.okhttp3 mockwebserver3 test org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test org.awaitility awaitility test org.junit.jupiter junit-jupiter-params test ch.qos.logback logback-classic test kr.motd.maven os-maven-plugin initialize detect org.xolstice.maven.plugins protobuf-maven-plugin compile test-compile com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/EndpointParams.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; /** * SonarQube or SonarCloud endpoint parameters */ public class EndpointParams { private final String baseUrl; @Nullable // For SonarQube Cloud, some APIs are located under a dedicated subdomain. Null for SonarQube Server private final String apiBaseUrl; private final boolean sonarCloud; @Nullable private final String organization; public EndpointParams(String baseUrl, @Nullable String apiBaseUrl, boolean isSonarCloud, @Nullable String organization) { this.baseUrl = baseUrl; this.apiBaseUrl = apiBaseUrl; this.sonarCloud = isSonarCloud; this.organization = organization; } public String getBaseUrl() { return baseUrl; } @CheckForNull public String getApiBaseUrl() { return apiBaseUrl; } public boolean isSonarCloud() { return sonarCloud; } /** * Organization can be missing even for SonarCloud, because some API calls are made before knowing the organization (like fetching user organizations) */ public Optional getOrganization() { return sonarCloud ? Optional.ofNullable(organization) : Optional.empty(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import java.util.Optional; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.serverapi.authentication.AuthenticationApi; import org.sonarsource.sonarlint.core.serverapi.branches.ProjectBranchesApi; import org.sonarsource.sonarlint.core.serverapi.component.ComponentApi; import org.sonarsource.sonarlint.core.serverapi.developers.DevelopersApi; import org.sonarsource.sonarlint.core.serverapi.features.FeaturesApi; import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.FixSuggestionsApi; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import org.sonarsource.sonarlint.core.serverapi.issue.IssueApi; import org.sonarsource.sonarlint.core.serverapi.newcode.NewCodeApi; import org.sonarsource.sonarlint.core.serverapi.organization.OrganizationApi; import org.sonarsource.sonarlint.core.serverapi.plugins.PluginsApi; import org.sonarsource.sonarlint.core.serverapi.projectbindings.ProjectBindingsApi; import org.sonarsource.sonarlint.core.serverapi.push.PushApi; import org.sonarsource.sonarlint.core.serverapi.qualityprofile.QualityProfileApi; import org.sonarsource.sonarlint.core.serverapi.rules.RulesApi; import org.sonarsource.sonarlint.core.serverapi.sca.ScaApi; import org.sonarsource.sonarlint.core.serverapi.settings.SettingsApi; import org.sonarsource.sonarlint.core.serverapi.source.SourceApi; import org.sonarsource.sonarlint.core.serverapi.system.SystemApi; import org.sonarsource.sonarlint.core.serverapi.users.UsersApi; public class ServerApi { private final ServerApiHelper helper; public ServerApi(EndpointParams endpoint, HttpClient client) { this(new ServerApiHelper(endpoint, client)); } public ServerApi(ServerApiHelper helper) { this.helper = helper; } public AuthenticationApi authentication() { return new AuthenticationApi(helper); } public ProjectBindingsApi projectBindings() { return new ProjectBindingsApi(helper); } public ComponentApi component() { return new ComponentApi(helper); } public DevelopersApi developers() { return new DevelopersApi(helper); } public HotspotApi hotspot() { return new HotspotApi(helper); } public OrganizationApi organization() { return new OrganizationApi(helper); } public IssueApi issue() { return new IssueApi(helper); } public SourceApi source() { return new SourceApi(helper); } public SettingsApi settings() { return new SettingsApi(helper); } public QualityProfileApi qualityProfile() { return new QualityProfileApi(helper); } public PluginsApi plugins() { return new PluginsApi(helper); } public RulesApi rules() { return new RulesApi(helper); } public SystemApi system() { return new SystemApi(helper); } public ProjectBranchesApi branches() { return new ProjectBranchesApi(helper); } public PushApi push() { return new PushApi(helper); } public NewCodeApi newCodeApi() { return new NewCodeApi(helper); } public FixSuggestionsApi fixSuggestions() { return new FixSuggestionsApi(helper); } public FeaturesApi features() { return new FeaturesApi(helper); } public ScaApi sca() { return new ScaApi(helper); } public UsersApi users() { return new UsersApi(helper); } public boolean isSonarCloud() { return helper.isSonarCloud(); } public Optional getOrganizationKey() { return helper.getOrganizationKey(); } public ServerApiHelper getHelper() { return helper; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelper.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.LongConsumer; import java.util.function.Supplier; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpConnectionListener; import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; import org.sonarsource.sonarlint.core.serverapi.exception.NetworkException; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.exception.TooManyRequestsException; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedServerResponseException; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.http.HttpClient.JSON_CONTENT_TYPE; /** * Wrapper around HttpClient to avoid repetitive code, like support of pagination, and log timing of requests */ public class ServerApiHelper { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final int PAGE_SIZE = 500; public static final int MAX_PAGES = 20; public static final int HTTP_TOO_MANY_REQUESTS = 429; private final HttpClient client; private final EndpointParams endpointParams; // avoid Gson replacing characters like < > or = with Unicode representation private static final Gson gson = new GsonBuilder() .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeDeserializer()) .disableHtmlEscaping() .create(); public ServerApiHelper(EndpointParams endpointParams, HttpClient client) { this.endpointParams = endpointParams; this.client = client; } public boolean isSonarCloud() { return endpointParams.isSonarCloud(); } public HttpClient.Response getAnonymous(String path, SonarLintCancelMonitor cancelMonitor) { var response = rawGetUrlAnonymous(buildEndpointUrl(path), cancelMonitor); if (!response.isSuccessful()) { throw handleError(response); } return response; } public T getAnonymousJson(String path, Class responseClass, SonarLintCancelMonitor cancelMonitor) { try (var response = getAnonymous(path, cancelMonitor)) { return deserializeJsonBody(response, responseClass); } } public HttpClient.Response get(String path, SonarLintCancelMonitor cancelMonitor) { var response = rawGet(path, cancelMonitor); if (!response.isSuccessful()) { throw handleError(response); } return response; } public T getJson(String path, Class responseClass, SonarLintCancelMonitor cancelMonitor) { try (var response = get(path, cancelMonitor)) { return deserializeJsonBody(response, responseClass); } } public T apiGetJson(String path, Class responseClass, SonarLintCancelMonitor cancelMonitor) { try (var response = rawGetUrl(buildApiEndpointUrl(path), cancelMonitor)) { if (!response.isSuccessful()) { throw handleError(response); } return deserializeJsonBody(response, responseClass); } } public HttpClient.Response post(String relativePath, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { return postUrl(buildEndpointUrl(relativePath), contentType, body, cancelMonitor); } public void postJson(String relativePath, Object requestBody, SonarLintCancelMonitor cancelMonitor) { postJson(relativePath, requestBody, null, cancelMonitor); } public T postJson(String relativePath, Object requestBody, @Nullable Class responseClass, SonarLintCancelMonitor cancelMonitor) { var body = gson.toJson(requestBody); try (var response = post(relativePath, JSON_CONTENT_TYPE, body, cancelMonitor)) { return responseClass == null ? null : deserializeJsonBody(response, responseClass); } } public void apiPostJson(String relativePath, Object requestBody, SonarLintCancelMonitor cancelMonitor) { apiPostJson(relativePath, requestBody, null, cancelMonitor); } public T apiPostJson(String relativePath, Object requestBody, @Nullable Class responseClass, SonarLintCancelMonitor cancelMonitor) { var body = gson.toJson(requestBody); try (var response = postUrl(buildApiEndpointUrl(relativePath), JSON_CONTENT_TYPE, body, cancelMonitor)) { return responseClass == null ? null : deserializeJsonBody(response, responseClass); } } private HttpClient.Response postUrl(String url, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { var response = rawPost(url, contentType, body, cancelMonitor); if (!response.isSuccessful()) { throw handleError(response); } return response; } /** * Execute GET and don't check response */ public HttpClient.Response rawGet(String relativePath, SonarLintCancelMonitor cancelMonitor) { return rawGetUrl(buildEndpointUrl(relativePath), cancelMonitor); } private HttpClient.Response rawGetUrl(String url, SonarLintCancelMonitor cancelMonitor) { var startTime = Instant.now(); var httpFuture = client.getAsync(url); return processResponse("GET", cancelMonitor, httpFuture, startTime, url); } private HttpClient.Response rawGetUrlAnonymous(String url, SonarLintCancelMonitor cancelMonitor) { var startTime = Instant.now(); var httpFuture = client.getAsyncAnonymous(url); return processResponse("GET", cancelMonitor, httpFuture, startTime, url); } public HttpClient.Response rawPost(String url, String contentType, String body, SonarLintCancelMonitor cancelMonitor) { var startTime = Instant.now(); var httpFuture = client.postAsync(url, contentType, body); return processResponse("POST", cancelMonitor, httpFuture, startTime, url); } private static HttpClient.Response processResponse(String method, SonarLintCancelMonitor cancelMonitor, CompletableFuture httpFuture, Instant startTime, String url) { cancelMonitor.onCancel(() -> httpFuture.cancel(true)); try { var response = httpFuture.join(); logTime(method, startTime, url, response.code()); return response; } catch (Exception e) { logFailure(method, startTime, url, e.getMessage()); throw new NetworkException("Request failed", e); } } private static void logTime(String method, Instant startTime, String url, int responseCode) { var duration = Duration.between(startTime, Instant.now()); LOG.debug("{} {} {} | response time={}ms", method, responseCode, url, duration.toMillis()); } private static void logFailure(String method, Instant startTime, String url, String message) { var duration = Duration.between(startTime, Instant.now()); LOG.debug("{} {} {} | failed after {}ms", method, url, message, duration.toMillis()); } private String buildEndpointUrl(String relativePath) { return concat(endpointParams.getBaseUrl(), relativePath); } private String buildApiEndpointUrl(String relativePath) { return concat(requireNonNull(endpointParams.getApiBaseUrl()), relativePath); } public static String concat(String baseUrl, String relativePath) { return Strings.CS.appendIfMissing(baseUrl, "/") + (relativePath.startsWith("/") ? relativePath.substring(1) : relativePath); } public static RuntimeException handleError(HttpClient.Response toBeClosed) { try (var failedResponse = toBeClosed) { if (failedResponse.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { return new UnauthorizedException("Not authorized. Please check server credentials."); } if (failedResponse.code() == HttpURLConnection.HTTP_FORBIDDEN) { // Details are in the response content var error = tryParseAsJsonError(failedResponse); if (error == null) { error = "Access denied"; } return new ForbiddenException(error); } if (failedResponse.code() == HttpURLConnection.HTTP_NOT_FOUND) { return new NotFoundException(formatHttpFailedResponse(failedResponse, null)); } if (failedResponse.code() >= HttpURLConnection.HTTP_INTERNAL_ERROR) { return new ServerErrorException(formatHttpFailedResponse(failedResponse, null)); } if (failedResponse.code() == HTTP_TOO_MANY_REQUESTS) { return new TooManyRequestsException("Too many requests have been made."); } var errorMsg = tryParseAsJsonError(failedResponse); return new UnexpectedServerResponseException(formatHttpFailedResponse(failedResponse, errorMsg)); } } private static String formatHttpFailedResponse(HttpClient.Response failedResponse, @Nullable String errorMsg) { return "Error " + failedResponse.code() + " on " + failedResponse.url() + (errorMsg != null ? (": " + errorMsg) : ""); } @CheckForNull private static String tryParseAsJsonError(HttpClient.Response response) { try { var content = response.bodyAsString(); if (StringUtils.isBlank(content)) { return null; } var obj = JsonParser.parseString(content).getAsJsonObject(); var errors = obj.getAsJsonArray("errors"); if (errors == null) { return null; } List errorMessages = new ArrayList<>(); for (JsonElement e : errors) { errorMessages.add(e.getAsJsonObject().get("msg").getAsString()); } return String.join(", ", errorMessages); } catch (Exception e) { LOG.error("Error parsing JSON error", e); } return null; } public Optional getOrganizationKey() { return endpointParams.getOrganization(); } public void getPaginated(String relativeUrlWithoutPaginationParams, CheckedFunction responseParser, Function getPagingTotal, Function> itemExtractor, Consumer itemConsumer, boolean limitToTwentyPages, SonarLintCancelMonitor cancelChecker) { getPaginated(relativeUrlWithoutPaginationParams, responseParser, getPagingTotal, itemExtractor, itemConsumer, limitToTwentyPages, cancelChecker, "p", "ps"); } public void getPaginated(String relativeUrlWithoutPaginationParams, CheckedFunction responseParser, Function getPagingTotal, Function> itemExtractor, Consumer itemConsumer, boolean limitToTwentyPages, SonarLintCancelMonitor cancelChecker, String pageFieldName, String pageSizeFieldName) { var baseUrl = buildEndpointUrl(relativeUrlWithoutPaginationParams); getPaginatedBaseUrl(baseUrl, responseParser, getPagingTotal, itemExtractor, itemConsumer, limitToTwentyPages, cancelChecker, pageFieldName, pageSizeFieldName); } public void apiGetPaginated(String relativeUrlWithoutPaginationParams, CheckedFunction responseParser, Function getPagingTotal, Function> itemExtractor, Consumer itemConsumer, boolean limitToTwentyPages, SonarLintCancelMonitor cancelChecker, String pageFieldName, String pageSizeFieldName) { var baseUrl = buildApiEndpointUrl(relativeUrlWithoutPaginationParams); getPaginatedBaseUrl(baseUrl, responseParser, getPagingTotal, itemExtractor, itemConsumer, limitToTwentyPages, cancelChecker, pageFieldName, pageSizeFieldName); } private void getPaginatedBaseUrl(String baseUrl, CheckedFunction responseParser, Function getPagingTotal, Function> itemExtractor, Consumer itemConsumer, boolean limitToTwentyPages, SonarLintCancelMonitor cancelChecker, String pageFieldName, String pageSizeFieldName) { var page = new AtomicInteger(0); var stop = new AtomicBoolean(false); var loaded = new AtomicInteger(0); do { page.incrementAndGet(); var fullUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") + pageSizeFieldName + "=" + PAGE_SIZE + "&" + pageFieldName + "=" + page; ServerApiHelper.consumeTimed( () -> rawGetUrl(fullUrl, cancelChecker), response -> processPage(baseUrl, responseParser, getPagingTotal, itemExtractor, itemConsumer, limitToTwentyPages, page, stop, loaded, response), duration -> LOG.debug("Page downloaded in {}ms", duration)); } while (!stop.get() && !cancelChecker.isCanceled()); } private static void processPage(String baseUrl, CheckedFunction responseParser, Function getPagingTotal, Function> itemExtractor, Consumer itemConsumer, boolean limitToTwentyPages, AtomicInteger page, AtomicBoolean stop, AtomicInteger loaded, HttpClient.Response response) throws IOException { if (!response.isSuccessful()) { throw handleError(response); } G protoBufResponse; try (var body = response.bodyAsStream()) { protoBufResponse = responseParser.apply(body); } var items = itemExtractor.apply(protoBufResponse); for (F item : items) { itemConsumer.accept(item); loaded.incrementAndGet(); } var isEmpty = items.isEmpty(); var pagingTotal = getPagingTotal.apply(protoBufResponse).longValue(); // SONAR-9150 Some WS used to miss the paging information, so iterate until response is empty stop.set(isEmpty || (pagingTotal > 0 && (long) page.get() * PAGE_SIZE >= pagingTotal)); if (!stop.get() && limitToTwentyPages && page.get() >= MAX_PAGES) { stop.set(true); LOG.debug("Limiting number of requested pages from '{}' to {}. Some of the data won't be fetched", baseUrl, MAX_PAGES); } } public HttpClient.AsyncRequest getEventStream(String path, HttpConnectionListener connectionListener, Consumer messageConsumer) { return client.getEventStream(buildEndpointUrl(path), connectionListener, messageConsumer); } @FunctionalInterface public interface CheckedFunction { R apply(T t) throws IOException; } public static G processTimed(Supplier responseSupplier, IOFunction responseProcessor, LongConsumer durationConsumer) { var startTime = Instant.now(); G result; try (var response = responseSupplier.get()) { result = responseProcessor.apply(response); } catch (IOException e) { throw new IllegalStateException("Unable to parse WS response: " + e.getMessage(), e); } durationConsumer.accept(Duration.between(startTime, Instant.now()).toMillis()); return result; } public static void consumeTimed(Supplier responseSupplier, IOConsumer responseConsumer, LongConsumer durationConsumer) { processTimed(responseSupplier, r -> { responseConsumer.accept(r); return null; }, durationConsumer); } @FunctionalInterface public interface IOFunction { R apply(T t) throws IOException; } @FunctionalInterface public interface IOConsumer { void accept(T t) throws IOException; } /** * Deserialize JSON response body to the specified type. * * @param response the HTTP response containing JSON body * @param responseClass the class to deserialize to * @return the deserialized object * @throws UnexpectedBodyException if the response body cannot be deserialized */ private static T deserializeJsonBody(HttpClient.Response response, Class responseClass) { try { var responseStr = response.bodyAsString(); return gson.fromJson(responseStr, responseClass); } catch (Exception e) { throw new UnexpectedBodyException(e); } } private static class ZonedDateTimeDeserializer implements JsonDeserializer { private static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); @Override public ZonedDateTime deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString(), TIME_FORMATTER); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/UrlUtils.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class UrlUtils { private UrlUtils() { } public static String urlEncode(String toEncode) { return URLEncoder.encode(toEncode, StandardCharsets.UTF_8); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/authentication/AuthenticationApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.authentication; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.system.ValidationResult; public class AuthenticationApi { private final ServerApiHelper serverApiHelper; public AuthenticationApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } public ValidationResult validate(SonarLintCancelMonitor cancelMonitor) { var validateResponse = serverApiHelper.getJson("api/authentication/validate?format=json", ValidateResponseDto.class, cancelMonitor); return new ValidationResult(validateResponse.valid(), validateResponse.valid() ? "Authentication successful" : "Authentication failed"); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/authentication/ValidateResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.authentication; public record ValidateResponseDto(boolean valid) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/authentication/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.authentication; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/branches/ProjectBranchesApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.branches; import java.util.List; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.BranchType; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.ProjectBranches; public class ProjectBranchesApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String LIST_ALL_PROJECT_BRANCHES_URL = "/api/project_branches/list.protobuf"; private final ServerApiHelper helper; public ProjectBranchesApi(ServerApiHelper helper) { this.helper = helper; } public List getAllBranches(String projectKey, SonarLintCancelMonitor cancelMonitor) { ProjectBranches.ListWsResponse response; try (var wsResponse = helper.get(LIST_ALL_PROJECT_BRANCHES_URL + "?project=" + UrlUtils.urlEncode(projectKey), cancelMonitor); var is = wsResponse.bodyAsStream()) { response = ProjectBranches.ListWsResponse.parseFrom(is); } catch (Exception e) { LOG.error("Error while fetching project branches", e); return List.of(); } return response.getBranchesList().stream() .filter(b -> b.getType() == BranchType.BRANCH || b.getType() == BranchType.LONG) .map(branchWs -> new ServerBranch(branchWs.getName(), branchWs.getIsMain())).toList(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/branches/ServerBranch.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.branches; public class ServerBranch { private final String name; private final boolean isMain; public ServerBranch(String name, boolean isMain) { this.name = name; this.isMain = isMain; } public String getName() { return name; } public boolean isMain() { return isMain; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/branches/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.branches; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/Component.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; public record Component(String key, String name, boolean isAiCodeFixEnabled) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/ComponentApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Components; public class ComponentApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String ORGANIZATION_PARAM = "&organization="; private final ServerApiHelper helper; public ComponentApi(ServerApiHelper helper) { this.helper = helper; } public List getAllFileKeys(String projectKey, SonarLintCancelMonitor cancelMonitor) { var path = buildAllFileKeysPath(projectKey); List files = new ArrayList<>(); helper.getPaginated(path, Components.TreeWsResponse::parseFrom, r -> r.getPaging().getTotal(), Components.TreeWsResponse::getComponentsList, component -> files.add(component.getKey()), false, cancelMonitor); return files; } private String buildAllFileKeysPath(String projectKey) { var url = new StringBuilder(); url.append("api/components/tree.protobuf?qualifiers=FIL,UTS&"); url.append("component=").append(UrlUtils.urlEncode(projectKey)); helper.getOrganizationKey().ifPresent(org -> url.append(ORGANIZATION_PARAM).append(UrlUtils.urlEncode(org))); return url.toString(); } public Optional getProject(String projectKey, SonarLintCancelMonitor cancelMonitor) { return fetchComponent(projectKey, cancelMonitor).map(component -> new ServerProject(component.key(), component.name(), component.isAiCodeFixEnabled())); } public List getAllProjects(SonarLintCancelMonitor cancelMonitor) { List serverProjects = new ArrayList<>(); helper.getPaginated(getAllProjectsUrl(), Components.SearchWsResponse::parseFrom, r -> r.getPaging().getTotal(), Components.SearchWsResponse::getComponentsList, project -> serverProjects.add(new ServerProject(project.getKey(), project.getName(), project.getIsAiCodeFixEnabled())), true, cancelMonitor); return serverProjects; } private String getAllProjectsUrl() { var searchUrl = new StringBuilder(); searchUrl.append("api/components/search.protobuf?qualifiers=TRK"); helper.getOrganizationKey() .ifPresent(org -> searchUrl.append(ORGANIZATION_PARAM).append(UrlUtils.urlEncode(org))); return searchUrl.toString(); } @CheckForNull public SearchProjectResponse searchProjects(String projectId, SonarLintCancelMonitor cancelMonitor) { var encodedProjectId = UrlUtils.urlEncode(projectId); var organization = helper.getOrganizationKey(); if (organization.isEmpty()) { LOG.warn("Organization key is not set, cannot search projects for ID: {}", projectId); return null; } var path = "/api/components/search_projects?projectIds=" + encodedProjectId + ORGANIZATION_PARAM + organization.get(); var searchResponse = helper.getJson(path, SearchProjectResponseDto.class, cancelMonitor); return searchResponse.components().stream() .findFirst() .map(component -> new SearchProjectResponse(component.key(), component.name())) .orElse(null); } private Optional fetchComponent(String componentKey, SonarLintCancelMonitor cancelMonitor) { return fetchComponent(componentKey, response -> { var wsComponent = response.getComponent(); return new Component(wsComponent.getKey(), wsComponent.getName(), wsComponent.getIsAiCodeFixEnabled()); }, cancelMonitor); } public Optional fetchFirstAncestorKey(String componentKey, SonarLintCancelMonitor cancelMonitor) { return fetchComponent(componentKey, response -> response.getAncestorsList().stream().map(Components.Component::getKey).findFirst().orElse(null), cancelMonitor); } private Optional fetchComponent(String componentKey, Function responseConsumer, SonarLintCancelMonitor cancelMonitor) { return ServerApiHelper.processTimed( () -> helper.rawGet("api/components/show.protobuf?component=" + UrlUtils.urlEncode(componentKey), cancelMonitor), response -> { if (response.isSuccessful()) { var wsResponse = Components.ShowWsResponse.parseFrom(response.bodyAsStream()); return Optional.ofNullable(responseConsumer.apply(wsResponse)); } return Optional.empty(); }, duration -> LOG.debug("Downloaded project details in {}ms", duration)); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/SearchProjectResponse.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; public record SearchProjectResponse(String projectKey, String projectName) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/SearchProjectResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; import java.util.List; public record SearchProjectResponseDto(List components) { public record ProjectComponent(String key, String name) {} } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/ServerProject.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; public record ServerProject(String key, String name, boolean isAiCodeFixEnabled) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/component/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.component; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/developers/DevelopersApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.developers; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; public class DevelopersApi { private static final String API_PATH = "api/developers/search_events"; public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); private final ServerApiHelper helper; public DevelopersApi(ServerApiHelper helper) { this.helper = helper; } public SearchEventsResponseDto searchEvents(Map projectTimestamps, SonarLintCancelMonitor cancelMonitor) { return helper.getJson(getWsPath(projectTimestamps), SearchEventsResponseDto.class, cancelMonitor); } private static String getWsPath(Map projectTimestamps) { // Sort project keys to simplify testing var sortedProjectKeys = projectTimestamps.keySet().stream().sorted().toList(); var builder = new StringBuilder(); builder.append(API_PATH); builder.append("?projects="); builder.append(sortedProjectKeys.stream() .map(UrlUtils::urlEncode) .collect(Collectors.joining(","))); builder.append("&from="); builder.append(sortedProjectKeys.stream() .map(projectTimestamps::get) .map(timestamp -> timestamp.format(TIME_FORMATTER)) .map(UrlUtils::urlEncode) .collect(Collectors.joining(","))); return builder.toString(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/developers/SearchEventsResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.developers; import java.time.ZonedDateTime; import java.util.List; import static java.util.Objects.requireNonNull; public record SearchEventsResponseDto(List events) { public record Event(String category, String message, String link, String project, ZonedDateTime date) { public Event { requireNonNull(category); requireNonNull(message); requireNonNull(link); requireNonNull(project); requireNonNull(date); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/developers/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.developers; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ForbiddenException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class ForbiddenException extends ServerRequestException { public ForbiddenException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/NetworkException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class NetworkException extends ServerRequestException { public NetworkException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/NotFoundException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class NotFoundException extends ServerRequestException { public NotFoundException(String msg) { super(msg); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ProjectNotFoundException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; import javax.annotation.Nullable; public class ProjectNotFoundException extends ServerRequestException { public ProjectNotFoundException(String moduleKey, @Nullable String organizationKey) { super(formatMessage(moduleKey, organizationKey)); } private static String formatMessage(String moduleKey, @Nullable String organizationKey) { if (organizationKey != null) { return String.format("Project with key '%s' in organization '%s' not found on SonarQube Cloud (was it deleted?)", moduleKey, organizationKey); } return String.format("Project with key '%s' not found on your SonarQube Server instance (was it deleted?)", moduleKey); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ServerErrorException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class ServerErrorException extends ServerRequestException { public ServerErrorException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/ServerRequestException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; import org.sonarsource.sonarlint.core.commons.SonarLintException; public class ServerRequestException extends SonarLintException { public ServerRequestException(String message) { super(message); } public ServerRequestException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/TooManyRequestsException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class TooManyRequestsException extends ServerRequestException { public TooManyRequestsException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnauthorizedException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class UnauthorizedException extends ServerRequestException { public UnauthorizedException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnexpectedBodyException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class UnexpectedBodyException extends ServerRequestException { public UnexpectedBodyException(Throwable cause) { super("Unexpected body received", cause); } public UnexpectedBodyException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnexpectedServerResponseException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; import org.sonarsource.sonarlint.core.commons.SonarLintException; public class UnexpectedServerResponseException extends SonarLintException { public UnexpectedServerResponseException(String message) { super(message); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/UnsupportedServerException.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; public class UnsupportedServerException extends ServerRequestException { public UnsupportedServerException(String msg) { super(msg); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/exception/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.exception; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/features/Feature.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.features; import java.util.Arrays; import java.util.Optional; public enum Feature { AI_CODE_FIX("fix-suggestions"), SCA("sca"); public static Optional fromKey(String key) { return Arrays.stream(values()).filter(f -> f.key.equals(key)).findFirst(); } private final String key; Feature(String key) { this.key = key; } public String getKey() { return key; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/features/FeaturesApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.features; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; public class FeaturesApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper helper; public FeaturesApi(ServerApiHelper helper) { this.helper = helper; } public Set list(SonarLintCancelMonitor cancelMonitor) { try { var featureKeys = helper.getJson("api/features/list", String[].class, cancelMonitor); return Arrays.stream(featureKeys).flatMap(key -> Feature.fromKey(key).stream()).collect(Collectors.toSet()); } catch (Exception e) { LOG.error("Error while fetching the list of features", e); throw e; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/features/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.features; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiCodeFixConfiguration.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import java.util.Set; import javax.annotation.Nullable; public record AiCodeFixConfiguration(SuggestionFeatureEnablement enablement, @Nullable Set enabledProjectKeys, boolean organizationEligible) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionRequestBodyDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import javax.annotation.Nullable; public record AiSuggestionRequestBodyDto(@Nullable String organizationKey, String projectKey, Issue issue) { public record Issue(String message, Integer startLine, Integer endLine, String ruleKey, String sourceCode) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/AiSuggestionResponseBodyDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import java.util.List; import java.util.UUID; public record AiSuggestionResponseBodyDto(UUID id, String explanation, List changes) { public record ChangeDto(int startLine, int endLine, String newCode) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.exception.TooManyRequestsException; public class FixSuggestionsApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper helper; public FixSuggestionsApi(ServerApiHelper helper) { this.helper = helper; } public AiSuggestionResponseBodyDto getAiSuggestion(AiSuggestionRequestBodyDto dto, SonarLintCancelMonitor cancelMonitor) { try { return helper.isSonarCloud() ? helper.apiPostJson("/fix-suggestions/ai-suggestions", dto, AiSuggestionResponseBodyDto.class, cancelMonitor) : helper.postJson("/api/v2/fix-suggestions/ai-suggestions", dto, AiSuggestionResponseBodyDto.class, cancelMonitor); } catch (TooManyRequestsException e) { throw e; } catch (Exception e) { LOG.error("Error while generating an AI CodeFix", e); throw e; } } public SupportedRulesResponseDto getSupportedRules(SonarLintCancelMonitor cancelMonitor) { try { return helper.isSonarCloud() ? helper.apiGetJson("/fix-suggestions/supported-rules", SupportedRulesResponseDto.class, cancelMonitor) : helper.getJson("/api/v2/fix-suggestions/supported-rules", SupportedRulesResponseDto.class, cancelMonitor); } catch (Exception e) { LOG.error("Error while fetching the list of AI CodeFix supported rules", e); throw e; } } public OrganizationConfigsResponseDto getOrganizationConfigs(String organizationId, SonarLintCancelMonitor cancelMonitor) { try { return helper.apiGetJson("/fix-suggestions/organization-configs/" + UrlUtils.urlEncode(organizationId), OrganizationConfigsResponseDto.class, cancelMonitor); } catch (Exception e) { LOG.error("Error while fetching the AI CodeFix organization config", e); throw e; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/OrganizationConfigsResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; public record OrganizationConfigsResponseDto(String organizationId, AiCodeFixConfiguration aiCodeFix) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/SuggestionFeatureEnablement.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; public enum SuggestionFeatureEnablement { DISABLED, ENABLED_FOR_ALL_PROJECTS, ENABLED_FOR_SOME_PROJECTS } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/SupportedRulesResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import java.util.Set; public record SupportedRulesResponseDto(Set rules) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/hotspot/HotspotApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.hotspot; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Hotspots; import org.sonarsource.sonarlint.core.serverapi.source.SourceApi; import org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils; import static org.sonarsource.sonarlint.core.http.HttpClient.FORM_URL_ENCODED_CONTENT_TYPE; import static org.sonarsource.sonarlint.core.serverapi.UrlUtils.urlEncode; import static org.sonarsource.sonarlint.core.serverapi.util.ProtobufUtil.readMessages; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.toSonarQubePath; public class HotspotApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final Version MIN_SQ_VERSION_SUPPORTING_PULL = Version.create("10.1"); private static final String HOTSPOTS_SEARCH_API_URL = "/api/hotspots/search.protobuf"; private static final String HOTSPOTS_SHOW_API_URL = "/api/hotspots/show.protobuf"; private static final String HOTSPOTS_PULL_API_URL = "/api/hotspots/pull"; private static final String PROJECT_KEY_QUERY_PARAM = "?projectKey="; private final ServerApiHelper helper; public HotspotApi(ServerApiHelper helper) { this.helper = helper; } public void changeStatus(String hotspotKey, HotspotReviewStatus status, SonarLintCancelMonitor cancelMonitor) { var isReviewed = status.isReviewed(); var webApiStatus = isReviewed ? "REVIEWED" : "TO_REVIEW"; var body = "hotspot=" + urlEncode(hotspotKey) + "&status=" + urlEncode(webApiStatus); if (isReviewed) { body += "&resolution=" + urlEncode(status.name()); } helper.post("api/hotspots/change_status", FORM_URL_ENCODED_CONTENT_TYPE, body, cancelMonitor); } public Collection getAll(String projectKey, String branchName, SonarLintCancelMonitor cancelMonitor) { return searchHotspots(getSearchUrl(projectKey, null, branchName), cancelMonitor); } public Collection getFromFile(String projectKey, Path filePath, String branchName, SonarLintCancelMonitor cancelMonitor) { return searchHotspots(getSearchUrl(projectKey, filePath, branchName), cancelMonitor); } public HotspotApi.HotspotsPullResult pullHotspots(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince , SonarLintCancelMonitor cancelMonitor) { return ServerApiHelper.processTimed( () -> helper.get(getPullHotspotsUrl(projectKey, branchName, enabledLanguages, changedSince), cancelMonitor), response -> { var input = response.bodyAsStream(); var timestamp = Hotspots.HotspotPullQueryTimestamp.parseDelimitedFrom(input); return new HotspotApi.HotspotsPullResult(timestamp, readMessages(input, Hotspots.HotspotLite.parser())); }, duration -> LOG.debug("Pulled issues in {}ms", duration)); } public static class HotspotsPullResult { private final Hotspots.HotspotPullQueryTimestamp timestamp; private final List hotspots; public HotspotsPullResult(Hotspots.HotspotPullQueryTimestamp timestamp, List hotspots) { this.timestamp = timestamp; this.hotspots = hotspots; } public Hotspots.HotspotPullQueryTimestamp getTimestamp() { return timestamp; } public List getHotspots() { return hotspots; } } private static String getPullHotspotsUrl(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince) { var enabledLanguageKeys = enabledLanguages.stream().map(SonarLanguage::getSonarLanguageKey).collect(Collectors.joining(",")); var url = new StringBuilder() .append(HOTSPOTS_PULL_API_URL) .append(PROJECT_KEY_QUERY_PARAM) .append(UrlUtils.urlEncode(projectKey)) .append("&branchName=") .append(UrlUtils.urlEncode(branchName)); if (!enabledLanguageKeys.isEmpty()) { url.append("&languages=").append(enabledLanguageKeys); } if (changedSince != null) { url.append("&changedSince=").append(changedSince); } return url.toString(); } public boolean supportHotspotsPull(Supplier serverVersion) { return supportHotspotsPull(helper.isSonarCloud(), serverVersion.get()); } public static boolean supportHotspotsPull(boolean isSonarCloud, Version serverVersion) { return !isSonarCloud && serverVersion.compareToIgnoreQualifier(HotspotApi.MIN_SQ_VERSION_SUPPORTING_PULL) >= 0; } private Collection searchHotspots(String searchUrl, SonarLintCancelMonitor cancelMonitor) { Collection hotspots = new ArrayList<>(); Map componentPathsByKey = new HashMap<>(); helper.getPaginated( searchUrl, Hotspots.SearchWsResponse::parseFrom, r -> r.getPaging().getTotal(), r -> { componentPathsByKey.clear(); componentPathsByKey.putAll(r.getComponentsList().stream().collect(Collectors.toMap(Hotspots.Component::getKey, component -> Path.of(component.getPath())))); return r.getHotspotsList(); }, hotspot -> { var filePath = componentPathsByKey.get(hotspot.getComponent()); if (filePath != null) { hotspots.add(adapt(hotspot, filePath)); } else { LOG.error("Error while fetching security hotspots, the component '" + hotspot.getComponent() + "' is missing"); } }, false, cancelMonitor); return hotspots; } private static String getSearchUrl(String projectKey, @Nullable Path filePath, String branchName) { return HOTSPOTS_SEARCH_API_URL + PROJECT_KEY_QUERY_PARAM + urlEncode(projectKey) + (filePath != null ? ("&files=" + urlEncode(toSonarQubePath(filePath))) : "") + "&branch=" + urlEncode(branchName); } public ServerHotspotDetails show(String hotspotKey, SonarLintCancelMonitor cancelMonitor) { try (var wsResponse = helper.get(getShowUrl(hotspotKey), cancelMonitor); var is = wsResponse.bodyAsStream()) { return adapt(Hotspots.ShowWsResponse.parseFrom(is), null); } catch (IOException e) { throw new UnexpectedBodyException(e); } } public Optional fetch(String hotspotKey, SonarLintCancelMonitor cancelMonitor) { Hotspots.ShowWsResponse response; try (var wsResponse = helper.get(getShowUrl(hotspotKey), cancelMonitor); var is = wsResponse.bodyAsStream()) { response = Hotspots.ShowWsResponse.parseFrom(is); } catch (Exception e) { LOG.error("Error while fetching security hotspot", e); return Optional.empty(); } var fileKey = response.getComponent().getKey(); var source = new SourceApi(helper).getRawSourceCode(fileKey, cancelMonitor); String codeSnippet; if (source.isPresent()) { try { codeSnippet = ServerApiUtils.extractCodeSnippet(source.get(), response.getTextRange()); } catch (Exception e) { LOG.debug("Unable to compute code snippet of '" + fileKey + "' for text range: " + response.getTextRange(), e); codeSnippet = null; } } else { codeSnippet = null; } return Optional.of(adapt(response, codeSnippet)); } private static ServerHotspotDetails adapt(Hotspots.ShowWsResponse hotspot, @Nullable String codeSnippet) { return new ServerHotspotDetails( hotspot.getMessage(), Path.of(hotspot.getComponent().getPath()), convertTextRange(hotspot.getTextRange()), hotspot.getAuthor(), ServerHotspotDetails.Status.valueOf(hotspot.getStatus()), hotspot.hasResolution() ? ServerHotspotDetails.Resolution.valueOf(hotspot.getResolution()) : null, adapt(hotspot.getRule()), codeSnippet, hotspot.getCanChangeStatus()); } private static ServerHotspotDetails.Rule adapt(Hotspots.Rule rule) { return new ServerHotspotDetails.Rule(rule.getKey(), rule.getName(), rule.getSecurityCategory(), VulnerabilityProbability.valueOf(rule.getVulnerabilityProbability()), rule.getRiskDescription(), rule.getVulnerabilityDescription(), rule.getFixRecommendations()); } private static ServerHotspot adapt(Hotspots.SearchWsResponse.Hotspot hotspot, Path filePath) { return new ServerHotspot( hotspot.getKey(), hotspot.getRuleKey(), hotspot.getMessage(), filePath, convertTextRange(hotspot.getTextRange()), ServerApiUtils.parseOffsetDateTime(hotspot.getCreationDate()).toInstant(), getStatus(hotspot), VulnerabilityProbability.valueOf(hotspot.getVulnerabilityProbability()), hotspot.getAssignee()); } private static HotspotReviewStatus getStatus(Hotspots.SearchWsResponse.Hotspot hotspot) { var status = hotspot.getStatus(); var resolution = hotspot.hasResolution() ? hotspot.getResolution() : null; return HotspotReviewStatus.fromStatusAndResolution(status, resolution); } private static String getShowUrl(String hotspotKey) { return HOTSPOTS_SHOW_API_URL + "?hotspot=" + urlEncode(hotspotKey); } private static TextRangeWithHash convertTextRange(Common.TextRange textRange) { return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartOffset(), textRange.getEndLine(), textRange.getEndOffset(), ""); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/hotspot/ServerHotspot.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.hotspot; import java.nio.file.Path; import java.time.Instant; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRange; public class ServerHotspot { private final UUID id; private final String key; private final String ruleKey; private final String message; private Path filePath; private final TextRange textRange; private final Instant creationDate; private HotspotReviewStatus status; private final VulnerabilityProbability vulnerabilityProbability; @Nullable private String assignee; public ServerHotspot(@Nullable UUID id, String key, String ruleKey, String message, Path filePath, TextRange textRange, Instant creationDate, HotspotReviewStatus status, VulnerabilityProbability vulnerabilityProbability, @Nullable String assignee) { this.id = id; this.key = key; this.ruleKey = ruleKey; this.message = message; this.filePath = filePath; this.textRange = textRange; this.creationDate = creationDate; this.status = status; this.vulnerabilityProbability = vulnerabilityProbability; this.assignee = assignee; } /** * constructor for backward compatibility, after finalization of migration from Xodus to H2 should not be used * when using with H2 UUID should always be set */ public ServerHotspot(String key, String ruleKey, String message, Path filePath, TextRange textRange, Instant creationDate, HotspotReviewStatus status, VulnerabilityProbability vulnerabilityProbability, @Nullable String assignee) { this(null, key, ruleKey, message, filePath, textRange, creationDate, status, vulnerabilityProbability, assignee); } @CheckForNull public UUID getId() { return id; } public void setFilePath(Path filePath) { this.filePath = filePath; } public String getKey() { return key; } public String getRuleKey() { return ruleKey; } public String getMessage() { return message; } public Path getFilePath() { return filePath; } public TextRange getTextRange() { return textRange; } public Instant getCreationDate() { return creationDate; } public HotspotReviewStatus getStatus() { return status; } public ServerHotspot withStatus(HotspotReviewStatus newStatus) { return new ServerHotspot(key, ruleKey, message, filePath, textRange, creationDate, newStatus, vulnerabilityProbability, assignee); } public VulnerabilityProbability getVulnerabilityProbability() { return vulnerabilityProbability; } public String getAssignee() { return assignee; } public void setStatus(HotspotReviewStatus status) { this.status = status; } public void setAssignee(String assignee) { this.assignee = assignee; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/hotspot/ServerHotspotDetails.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.hotspot; import java.nio.file.Path; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRange; public class ServerHotspotDetails { @Deprecated(forRemoval = true) public final String message; public final Path filePath; @Deprecated(forRemoval = true) public final TextRange textRange; @Deprecated(forRemoval = true) public final String author; @Deprecated(forRemoval = true) public final Status status; @Deprecated(forRemoval = true) @CheckForNull public final Resolution resolution; @Deprecated(forRemoval = true) public final Rule rule; @Deprecated(forRemoval = true) @CheckForNull public final String codeSnippet; public final boolean canChangeStatus; public ServerHotspotDetails(String message, Path filePath, TextRange textRange, String author, Status status, @Nullable Resolution resolution, Rule rule, @Nullable String codeSnippet, boolean canChangeStatus) { this.message = message; this.filePath = filePath; this.textRange = textRange; this.author = author; this.status = status; this.resolution = resolution; this.rule = rule; this.codeSnippet = codeSnippet; this.canChangeStatus = canChangeStatus; } @Deprecated(forRemoval = true) public static class Rule { public final String key; public final String name; public final String securityCategory; public final VulnerabilityProbability vulnerabilityProbability; public final String riskDescription; public final String vulnerabilityDescription; public final String fixRecommendations; public Rule(String key, String name, String securityCategory, VulnerabilityProbability vulnerabilityProbability, String riskDescription, String vulnerabilityDescription, String fixRecommendations) { this.key = key; this.name = name; this.securityCategory = securityCategory; this.vulnerabilityProbability = vulnerabilityProbability; this.riskDescription = riskDescription; this.vulnerabilityDescription = vulnerabilityDescription; this.fixRecommendations = fixRecommendations; } } @Deprecated(forRemoval = true) public enum Status { TO_REVIEW("To review"), REVIEWED("Reviewed"); Status(String description) { this.description = description; } public final String description; } @Deprecated(forRemoval = true) public enum Resolution { FIXED("fixed"), SAFE("safe"), ACKNOWLEDGED("acknowledged"); Resolution(String description) { this.description = description; } public final String description; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/hotspot/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.hotspot; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/issue/IssueApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.issue; import com.google.gson.Gson; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonar.scanner.protocol.input.ScannerInput; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.Transition; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.Component; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.Issue; import org.sonarsource.sonarlint.core.serverapi.source.SourceApi; import org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils; import static java.util.Objects.requireNonNull; import static org.sonarsource.sonarlint.core.http.HttpClient.FORM_URL_ENCODED_CONTENT_TYPE; import static org.sonarsource.sonarlint.core.http.HttpClient.JSON_CONTENT_TYPE; import static org.sonarsource.sonarlint.core.serverapi.UrlUtils.urlEncode; import static org.sonarsource.sonarlint.core.serverapi.util.ProtobufUtil.readMessages; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.toSonarQubePath; public class IssueApi { private static final Map transitionByStatus = Map.of( IssueStatus.ACCEPT, Transition.ACCEPT, IssueStatus.WONT_FIX, Transition.WONT_FIX, IssueStatus.FALSE_POSITIVE, Transition.FALSE_POSITIVE ); private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String ORGANIZATION_PARAM = "&organization="; private final ServerApiHelper serverApiHelper; public IssueApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } /** * Fetch vulnerabilities of the component with specified key. * If the component doesn't exist or it exists but has no issues, an empty iterator is returned. * * @param key project key, or file key. */ public DownloadIssuesResult downloadVulnerabilitiesForRules(String key, Set ruleKeys, @Nullable String branchName, SonarLintCancelMonitor cancelMonitor) { var searchUrl = new StringBuilder(); searchUrl.append(getVulnerabilitiesUrl(key, ruleKeys)); searchUrl.append(getUrlBranchParameter(branchName)); serverApiHelper.getOrganizationKey() .ifPresent(org -> searchUrl.append(ORGANIZATION_PARAM).append(UrlUtils.urlEncode(org))); List result = new ArrayList<>(); Map componentsPathByKey = new HashMap<>(); serverApiHelper.getPaginated(searchUrl.toString(), Issues.SearchWsResponse::parseFrom, r -> r.getPaging().getTotal(), r -> { componentsPathByKey.clear(); // Ignore project level issues componentsPathByKey.putAll(r.getComponentsList().stream().filter(Component::hasPath) .collect(Collectors.toMap(Component::getKey, component -> Path.of(component.getPath())))); return r.getIssuesList(); }, result::add, true, cancelMonitor); return new DownloadIssuesResult(result, componentsPathByKey); } public static class DownloadIssuesResult { private final List issues; private final Map componentPathsByKey; private DownloadIssuesResult(List issues, Map componentPathsByKey) { this.issues = issues; this.componentPathsByKey = componentPathsByKey; } public List getIssues() { return issues; } public Map getComponentPathsByKey() { return componentPathsByKey; } } private static String getVulnerabilitiesUrl(String key, Set ruleKeys) { var encodedKey = urlEncode(key); return "/api/issues/search.protobuf?statuses=OPEN,CONFIRMED,REOPENED,RESOLVED&types=VULNERABILITY&componentKeys=" + encodedKey + "&components=" + encodedKey + "&rules=" + urlEncode(String.join(",", ruleKeys)); } private static String getUrlBranchParameter(@Nullable String branchName) { if (branchName != null) { return "&branch=" + urlEncode(branchName); } return ""; } public List downloadAllFromBatchIssues(String key, @Nullable String branchName, SonarLintCancelMonitor cancelMonitor) { String batchIssueUrl = getBatchIssuesUrl(key) + getUrlBranchParameter(branchName); return ServerApiHelper.processTimed( () -> serverApiHelper.rawGet(batchIssueUrl, cancelMonitor), response -> { if (response.code() == 403 || response.code() == 404) { return Collections.emptyList(); } else if (!response.isSuccessful()) { throw ServerApiHelper.handleError(response); } var input = response.bodyAsStream(); var parser = ScannerInput.ServerIssue.parser(); return readMessages(input, parser); }, duration -> LOG.debug("Downloaded issues in {}ms", duration)); } private static String getBatchIssuesUrl(String key) { return "/batch/issues?key=" + UrlUtils.urlEncode(key); } private static String getPullIssuesUrl(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince) { var enabledLanguageKeys = enabledLanguages.stream().map(SonarLanguage::getSonarLanguageKey).collect(Collectors.joining(",")); var url = new StringBuilder() .append("/api/issues/pull?projectKey=") .append(UrlUtils.urlEncode(projectKey)).append("&branchName=").append(UrlUtils.urlEncode(branchName)); if (!enabledLanguageKeys.isEmpty()) { url.append("&languages=").append(enabledLanguageKeys); } if (changedSince != null) { url.append("&changedSince=").append(changedSince); } return url.toString(); } public IssuesPullResult pullIssues(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince, SonarLintCancelMonitor cancelMonitor) { return ServerApiHelper.processTimed( () -> serverApiHelper.get(getPullIssuesUrl(projectKey, branchName, enabledLanguages, changedSince), cancelMonitor), response -> { var input = response.bodyAsStream(); var timestamp = Issues.IssuesPullQueryTimestamp.parseDelimitedFrom(input); return new IssuesPullResult(timestamp, readMessages(input, Issues.IssueLite.parser())); }, duration -> LOG.debug("Pulled issues in {}ms", duration)); } public static class IssuesPullResult { private final Issues.IssuesPullQueryTimestamp timestamp; private final List issues; public IssuesPullResult(Issues.IssuesPullQueryTimestamp timestamp, List issues) { this.timestamp = timestamp; this.issues = issues; } public Issues.IssuesPullQueryTimestamp getTimestamp() { return timestamp; } public List getIssues() { return issues; } } private static String getPullTaintIssuesUrl(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince) { var enabledLanguageKeys = enabledLanguages.stream().map(SonarLanguage::getSonarLanguageKey).collect(Collectors.joining(",")); var url = new StringBuilder() .append("/api/issues/pull_taint?projectKey=") .append(UrlUtils.urlEncode(projectKey)).append("&branchName=").append(UrlUtils.urlEncode(branchName)); if (!enabledLanguageKeys.isEmpty()) { url.append("&languages=").append(enabledLanguageKeys); } if (changedSince != null) { url.append("&changedSince=").append(changedSince); } return url.toString(); } public TaintIssuesPullResult pullTaintIssues(String projectKey, String branchName, Set enabledLanguages, @Nullable Long changedSince, SonarLintCancelMonitor cancelMonitor) { return ServerApiHelper.processTimed( () -> serverApiHelper.get(getPullTaintIssuesUrl(projectKey, branchName, enabledLanguages, changedSince), cancelMonitor), response -> { var input = response.bodyAsStream(); var timestamp = Issues.TaintVulnerabilityPullQueryTimestamp.parseDelimitedFrom(input); return new TaintIssuesPullResult(timestamp, readMessages(input, Issues.TaintVulnerabilityLite.parser())); }, duration -> LOG.debug("Pulled taint issues in {}ms", duration)); } public void changeStatus(String issueKey, Transition transition, SonarLintCancelMonitor cancelMonitor) { var body = "issue=" + urlEncode(issueKey) + "&transition=" + urlEncode(transition.getStatus()); serverApiHelper.post("/api/issues/do_transition", FORM_URL_ENCODED_CONTENT_TYPE, body, cancelMonitor); } public void addComment(String issueKey, String text, SonarLintCancelMonitor cancelMonitor) { var body = "issue=" + urlEncode(issueKey) + "&text=" + urlEncode(text); serverApiHelper.post("/api/issues/add_comment", FORM_URL_ENCODED_CONTENT_TYPE, body, cancelMonitor); } public Issue searchByKey(String issueKey, SonarLintCancelMonitor cancelMonitor) { var searchUrl = new StringBuilder(); searchUrl.append("/api/issues/search.protobuf?issues=").append(urlEncode(issueKey)).append("&additionalFields=transitions"); serverApiHelper.getOrganizationKey() .ifPresent(org -> searchUrl.append(ORGANIZATION_PARAM).append(UrlUtils.urlEncode(org))); searchUrl.append("&ps=1&p=1"); try (var wsResponse = serverApiHelper.get(searchUrl.toString(), cancelMonitor); var body = wsResponse.bodyAsStream()) { var pbResponse = Issues.SearchWsResponse.parseFrom(body); if (pbResponse.getIssuesList().isEmpty()) { throw new UnexpectedBodyException("No issue found with key '" + issueKey + "'"); } return pbResponse.getIssuesList().get(0); } catch (IOException e) { LOG.error("Error when searching issue + '" + issueKey + "'", e); throw new UnexpectedBodyException(e); } } public Optional fetchServerIssue(String issueKey, String projectKey, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { String searchUrl = "/api/issues/search.protobuf?issues=" + urlEncode(issueKey) + "&componentKeys=" + projectKey + "&components=" + projectKey + "&ps=1&p=1"; if (pullRequest != null && !pullRequest.isEmpty()) { searchUrl = searchUrl.concat("&pullRequest=").concat(urlEncode(pullRequest)); } else if (!branch.isEmpty()) { // If we do have a pullRequest, no need to pass branch too searchUrl = searchUrl.concat("&branch=").concat(urlEncode(branch)); } try (var wsResponse = serverApiHelper.get(searchUrl, cancelMonitor); var is = wsResponse.bodyAsStream()) { var response = Issues.SearchWsResponse.parseFrom(is); if (response.getIssuesList().isEmpty() || response.getComponentsList().isEmpty()) { LOG.warn("No issue found with key '" + issueKey + "'"); return Optional.empty(); } var issue = response.getIssuesList().get(0); var optionalComponentWithPath = response.getComponentsList().stream().filter(component -> component.getKey().equals(issue.getComponent())).findFirst(); if (optionalComponentWithPath.isEmpty()) { LOG.warn("No path found in components for the issue with key '" + issueKey + "'"); return Optional.empty(); } var fileKey = issue.getComponent(); var codeSnippet = getCodeSnippet(fileKey, issue.getTextRange(), branch, pullRequest, cancelMonitor); return Optional.of(new ServerIssueDetails(issue, Path.of(optionalComponentWithPath.get().getPath()), response.getComponentsList(), codeSnippet.orElse(""))); } catch (Exception e) { LOG.warn("Error while fetching issue", e.getMessage()); return Optional.empty(); } } public Optional getCodeSnippet(String fileKey, Common.TextRange textRange, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { var source = new SourceApi(serverApiHelper).getRawSourceCodeForBranchAndPullRequest(fileKey, branch, pullRequest, cancelMonitor); if (source.isPresent()) { try { var codeSnippet = ServerApiUtils.extractCodeSnippet(source.get(), textRange); return Optional.of(codeSnippet); } catch (Exception e) { LOG.debug("Unable to compute code snippet of '" + fileKey + "' for text range: " + textRange, e); return Optional.empty(); } } else { return Optional.empty(); } } public void anticipatedTransitions(String projectKey, List resolvedLocalOnlyIssues, SonarLintCancelMonitor cancelMonitor) { serverApiHelper.post("/api/issues/anticipated_transitions?projectKey=" + projectKey, JSON_CONTENT_TYPE, new Gson().toJson(adapt(resolvedLocalOnlyIssues)), cancelMonitor); } private static List adapt(List resolvedLocalOnlyIssues) { return resolvedLocalOnlyIssues.stream().map(IssueApi::adapt).toList(); } private static IssueAnticipatedTransition adapt(LocalOnlyIssue issue) { Integer lineNumber = null; String lineHash = null; var lineWithHash = issue.getLineWithHash(); if (lineWithHash != null) { lineNumber = lineWithHash.getNumber(); lineHash = lineWithHash.getHash(); } var resolution = requireNonNull(issue.getResolution()); return new IssueAnticipatedTransition(toSonarQubePath(issue.getServerRelativePath()), lineNumber, lineHash, issue.getRuleKey(), issue.getMessage(), transitionByStatus.get(resolution.getStatus()).getStatus(), resolution.getComment()); } public static class TaintIssuesPullResult { private final Issues.TaintVulnerabilityPullQueryTimestamp timestamp; private final List issues; public TaintIssuesPullResult(Issues.TaintVulnerabilityPullQueryTimestamp timestamp, List issues) { this.timestamp = timestamp; this.issues = issues; } public Issues.TaintVulnerabilityPullQueryTimestamp getTimestamp() { return timestamp; } public List getTaintIssues() { return issues; } } public static class ServerIssueDetails { public final String key; public final String ruleKey; public final String codeSnippet; public final String creationDate; public final String message; public final Path path; public final Common.TextRange textRange; public final List flowList; public final List componentsList; public ServerIssueDetails(Issue issue, Path path, List componentsList, String codeSnippet) { this.key = issue.getKey(); this.ruleKey = issue.getRule(); this.textRange = issue.getTextRange(); this.path = path; this.flowList = issue.getFlowsList(); this.message = issue.getMessage(); this.creationDate = issue.getCreationDate(); this.componentsList = componentsList; this.codeSnippet = codeSnippet; } } private static class IssueAnticipatedTransition { public final String filePath; public final Integer line; public final String hash; public final String ruleKey; public final String issueMessage; public final String transition; public final String comment; private IssueAnticipatedTransition(String filePath, @Nullable Integer line, @Nullable String hash, String ruleKey, String issueMessage, String transition, @Nullable String comment) { this.filePath = filePath; this.line = line; this.hash = hash; this.ruleKey = ruleKey; this.issueMessage = issueMessage; this.transition = transition; this.comment = comment; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/issue/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.issue; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/newcode/NewCodeApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.newcode; import java.util.Optional; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Measures; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.parseOffsetDateTime; public class NewCodeApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String GET_NEW_CODE_DEFINITION_URL = "/api/measures/component.protobuf"; private static final String OLD_SQ_OR_SC_PERIOD = "periods"; private static final String NEW_SQ_PERIOD = "period"; private static final Version NEW_SQ_VERSION = Version.create("8.1"); private final ServerApiHelper helper; public NewCodeApi(ServerApiHelper helper) { this.helper = helper; } public Optional getNewCodeDefinition(String projectKey, @Nullable String branch, Version serverVersion, SonarLintCancelMonitor cancelMonitor) { Measures.ComponentWsResponse response; var period = getPeriodForServer(helper, serverVersion); var requestPath = new StringBuilder().append(GET_NEW_CODE_DEFINITION_URL) .append("?additionalFields=") .append(period) .append("&metricKeys=projects&component=") .append(UrlUtils.urlEncode(projectKey)); if (branch != null) { requestPath.append("&branch=").append(UrlUtils.urlEncode(branch)); } try ( var wsResponse = helper.get(requestPath.toString(), cancelMonitor); var is = wsResponse.bodyAsStream()) { response = Measures.ComponentWsResponse.parseFrom(is); } catch (Exception e) { LOG.error("Error while fetching new code definition", e); return Optional.empty(); } var periodFromWs = getPeriodFromWs(response); var modeString = periodFromWs.getMode(); var parameter = periodFromWs.hasParameter() ? periodFromWs.getParameter() : null; if (modeString.equals("REFERENCE_BRANCH") && parameter != null) { return Optional.of(NewCodeDefinition.withReferenceBranch(parameter)); } var date = periodFromWs.hasDate() ? parseOffsetDateTime(periodFromWs.getDate()).toInstant().toEpochMilli() : 0; if ((modeString.equals("NUMBER_OF_DAYS") || modeString.equals("days")) && parameter != null) { var days = Integer.parseInt(parameter); return Optional.of(NewCodeDefinition.withNumberOfDaysWithDate(days, date)); } if (modeString.equalsIgnoreCase("PREVIOUS_VERSION")) { return Optional.of(NewCodeDefinition.withPreviousVersion(date, parameter)); } if (modeString.equals("SPECIFIC_ANALYSIS") || modeString.equals("version") || modeString.equals("date")) { return Optional.of(NewCodeDefinition.withSpecificAnalysis(date)); } LOG.warn("Unsupported mode of new code definition: " + modeString); return Optional.empty(); } static Measures.Period getPeriodFromWs(Measures.ComponentWsResponse response) { if (response.hasPeriods()) { return response.getPeriods().getPeriods(0); } return response.getPeriod(); } static String getPeriodForServer(ServerApiHelper helper, Version serverVersion) { if (helper.isSonarCloud()) { return OLD_SQ_OR_SC_PERIOD; } if (serverVersion.compareToIgnoreQualifier(NEW_SQ_VERSION) < 0) { return OLD_SQ_OR_SC_PERIOD; } return NEW_SQ_PERIOD; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/newcode/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.newcode; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/organization/GetOrganizationsResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.organization; import java.util.UUID; public record GetOrganizationsResponseDto(String id, UUID uuidV4) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/organization/OrganizationApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.organization; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations; public class OrganizationApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper helper; public OrganizationApi(ServerApiHelper helper) { this.helper = helper; } public List listUserOrganizations(SonarLintCancelMonitor cancelMonitor) { var url = "api/organizations/search.protobuf?member=true"; return getPaginatedOrganizations(url, cancelMonitor); } public Optional searchOrganization(String organizationKey, SonarLintCancelMonitor cancelMonitor) { var url = "api/organizations/search.protobuf?organizations=" + UrlUtils.urlEncode(organizationKey); return getPaginatedOrganizations(url, cancelMonitor) .stream() .findFirst(); } public GetOrganizationsResponseDto getOrganizationByKey(SonarLintCancelMonitor cancelMonitor) { var organizationKey = helper.getOrganizationKey().orElseThrow(() -> new IllegalArgumentException("Organizations are only supported for SonarQube Cloud")); try { return helper.apiGetJson("/organizations/organizations?organizationKey=" + UrlUtils.urlEncode(organizationKey) + "&excludeEligibility=true", GetOrganizationsResponseDto[].class, cancelMonitor)[0]; } catch (Exception e) { LOG.error("Error while fetching the organization", e); throw e; } } private List getPaginatedOrganizations(String url, SonarLintCancelMonitor cancelMonitor) { List result = new ArrayList<>(); helper.getPaginated(url, Organizations.SearchWsResponse::parseFrom, r -> r.getPaging().getTotal(), Organizations.SearchWsResponse::getOrganizationsList, org -> result.add(new ServerOrganization(org)), false, cancelMonitor); return result; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/organization/ServerOrganization.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.organization; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.Organization; public class ServerOrganization { private final String key; private final String name; private final String description; public ServerOrganization(Organization org) { this.key = org.getKey(); this.name = org.getName(); this.description = org.getDescription(); } public String getKey() { return key; } public String getName() { return name; } public String getDescription() { return description; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/organization/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.organization; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/plugins/InstalledPluginsPayloadDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.plugins; public record InstalledPluginsPayloadDto(InstalledPluginPayloadDto[] plugins) { public record InstalledPluginPayloadDto(String key, String hash, String filename, boolean sonarLintSupported) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/plugins/PluginsApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.plugins; import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; public class PluginsApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String API_PLUGINS_INSTALLED_PATH = "/api/plugins/installed"; private final ServerApiHelper helper; public PluginsApi(ServerApiHelper helper) { this.helper = helper; } public List getInstalled(SonarLintCancelMonitor cancelMonitor) { var start = System.currentTimeMillis(); var plugins = helper.isSonarCloud() ? helper.getAnonymousJson(API_PLUGINS_INSTALLED_PATH, InstalledPluginsPayloadDto.class, cancelMonitor) : helper.getJson(API_PLUGINS_INSTALLED_PATH, InstalledPluginsPayloadDto.class, cancelMonitor); var result = Arrays.stream(plugins.plugins()).map(PluginsApi::toInstalledPlugin).toList(); var duration = System.currentTimeMillis() - start; LOG.info("Downloaded plugin list in {}ms", duration); return result; } private static ServerPlugin toInstalledPlugin(InstalledPluginsPayloadDto.InstalledPluginPayloadDto payload) { return new ServerPlugin(payload.key(), payload.hash(), payload.filename(), payload.sonarLintSupported()); } public void getPlugin(String key, Consumer pluginFileConsumer, SonarLintCancelMonitor cancelMonitor) { var url = "api/plugins/download?plugin=" + key; var start = System.currentTimeMillis(); try (var response = get(url, cancelMonitor)) { pluginFileConsumer.accept(response.bodyAsStream()); var duration = System.currentTimeMillis() - start; LOG.info("Downloaded '{}' in {}ms", key, duration); } } private HttpClient.Response get(String path, SonarLintCancelMonitor cancelMonitor) { return helper.isSonarCloud() ? helper.getAnonymous(path, cancelMonitor) : helper.get(path, cancelMonitor); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/plugins/ServerPlugin.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.plugins; public class ServerPlugin { private final String key; private final String hash; private final String filename; private final boolean sonarLintSupported; public ServerPlugin(String key, String hash, String filename, boolean sonarLintSupported) { this.key = key; this.hash = hash; this.filename = filename; this.sonarLintSupported = sonarLintSupported; } public String getKey() { return key; } public String getHash() { return hash; } public String getFilename() { return filename; } public boolean isSonarLintSupported() { return sonarLintSupported; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/plugins/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.plugins; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/ProjectBindingsApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; public class ProjectBindingsApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper serverApiHelper; public ProjectBindingsApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } @CheckForNull public SQCProjectBindingsResponse getSQCProjectBindings(String url, SonarLintCancelMonitor cancelMonitor) { var encodedUrl = UrlUtils.urlEncode(url); var path = "/dop-translation/project-bindings?url=" + encodedUrl; try { var dto = serverApiHelper.apiGetJson(path, SQCProjectBindingsResponseDto.class, cancelMonitor); var bindings = dto.bindings(); if (!bindings.isEmpty()) { return new SQCProjectBindingsResponse(bindings.get(0).projectId()); } } catch (Exception e) { LOG.error("Error retrieving project bindings for URL: {}", url, e); } return null; } @CheckForNull public SQSProjectBindingsResponse getSQSProjectBindings(String url, SonarLintCancelMonitor cancelMonitor) { var encodedUrl = UrlUtils.urlEncode(url); var dto = serverApiHelper.getJson("/api/v2/dop-translation/project-bindings?repositoryUrl=" + encodedUrl, SQSProjectBindingsResponseDto.class, cancelMonitor); var bindings = dto.projectBindings(); if (!bindings.isEmpty()) { return new SQSProjectBindingsResponse(dto.projectBindings().get(0).projectId(), dto.projectBindings().get(0).projectKey()); } return null; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/SQCProjectBindingsResponse.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; public record SQCProjectBindingsResponse(String projectId) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/SQCProjectBindingsResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; import java.util.List; public record SQCProjectBindingsResponseDto(List bindings) { public record Binding(String projectId) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/SQSProjectBindingsResponse.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; public record SQSProjectBindingsResponse(String projectId, String projectKey) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/SQSProjectBindingsResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; import java.util.List; public record SQSProjectBindingsResponseDto(List projectBindings) { public record ProjectBinding(String projectId, String projectKey) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.projectbindings; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/IssueChangedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; public class IssueChangedEvent implements SonarProjectEvent { private final String projectKey; private final List impactedIssues; private final IssueSeverity userSeverity; private final RuleType userType; private final Boolean resolved; public IssueChangedEvent(String projectKey, List impactedIssues, @Nullable IssueSeverity userSeverity, @Nullable RuleType userType, @Nullable Boolean resolved) { this.projectKey = projectKey; this.impactedIssues = impactedIssues; this.userSeverity = userSeverity; this.userType = userType; this.resolved = resolved; } @Override public String getProjectKey() { return projectKey; } public List getImpactedIssues() { return impactedIssues; } /** * @return null when not changed */ @CheckForNull public IssueSeverity getUserSeverity() { return userSeverity; } /** * @return null when not changed */ @CheckForNull public RuleType getUserType() { return userType; } /** * @return null when not changed */ @CheckForNull public Boolean getResolved() { return resolved; } public static class Issue { private final String issueKey; private final String branchName; private final Map impacts; public Issue(String issueKey, String branchName, Map impacts) { this.issueKey = issueKey; this.branchName = branchName; this.impacts = impacts; } public String getIssueKey() { return issueKey; } public String getBranchName() { return branchName; } public Map getImpacts() { return impacts; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/PushApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.push.parsing.EventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.IssueChangedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.RuleSetChangedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotChangedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotClosedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.SecurityHotspotRaisedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.TaintVulnerabilityClosedEventParser; import org.sonarsource.sonarlint.core.serverapi.push.parsing.TaintVulnerabilityRaisedEventParser; import org.sonarsource.sonarlint.core.serverapi.stream.Event; import org.sonarsource.sonarlint.core.serverapi.stream.EventStream; public class PushApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String API_PATH = "api/push/sonarlint_events"; private static final Map> parsersByType = Map.of( "RuleSetChanged", new RuleSetChangedEventParser(), "IssueChanged", new IssueChangedEventParser(), "TaintVulnerabilityRaised", new TaintVulnerabilityRaisedEventParser(), "TaintVulnerabilityClosed", new TaintVulnerabilityClosedEventParser(), "SecurityHotspotRaised", new SecurityHotspotRaisedEventParser(), "SecurityHotspotChanged", new SecurityHotspotChangedEventParser(), "SecurityHotspotClosed", new SecurityHotspotClosedEventParser()); private final ServerApiHelper helper; public PushApi(ServerApiHelper helper) { this.helper = helper; } public EventStream subscribe(Set projectKeys, Set enabledLanguages, Consumer serverEventConsumer) { return new EventStream(helper, rawEvent -> handleRawEvent(rawEvent, serverEventConsumer)) .connect(getWsPath(projectKeys, enabledLanguages)); } private static String getWsPath(Set projectKeys, Set enabledLanguages) { return API_PATH + "?projectKeys=" + projectKeys.stream().map(UrlUtils::urlEncode).collect(Collectors.joining(",")) + "&languages=" + enabledLanguages.stream().map(SonarLanguage::getSonarLanguageKey).map(UrlUtils::urlEncode).collect(Collectors.joining(",")); } private static void handleRawEvent(Event rawEvent, Consumer serverEventConsumer) { LOG.debug("Server event received: {}", rawEvent); parse(rawEvent).ifPresent(serverEventConsumer); } private static Optional parse(Event event) { var eventType = event.getType(); if (!parsersByType.containsKey(eventType)) { LOG.error("Unknown '{}' event type ", eventType); return Optional.empty(); } try { return parsersByType.get(eventType).parse(event.getData()); } catch (Exception e) { LOG.error("Cannot parse '{}' received event", eventType, e); } return Optional.empty(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/RuleSetChangedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.util.List; import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; public class RuleSetChangedEvent implements SonarServerEvent { private final List projectKeys; private final List activatedRules; private final List deactivatedRules; public RuleSetChangedEvent(List projectKeys, List activatedRules, List deactivatedRules) { this.projectKeys = projectKeys; this.activatedRules = activatedRules; this.deactivatedRules = deactivatedRules; } public List getProjectKeys() { return projectKeys; } public List getActivatedRules() { return activatedRules; } public List getDeactivatedRules() { return deactivatedRules; } public static class ActiveRule { private final String key; private final String languageKey; private final IssueSeverity severity; private final Map parameters; private final String templateKey; private final List overridenImpacts; public ActiveRule(String key, String languageKey, IssueSeverity severity, Map parameters, @Nullable String templateKey, List overridenImpacts) { this.key = key; this.languageKey = languageKey; this.severity = severity; this.parameters = parameters; this.templateKey = templateKey; this.overridenImpacts = overridenImpacts; } public String getKey() { return key; } public String getLanguageKey() { return languageKey; } public IssueSeverity getSeverity() { return severity; } public Map getParameters() { return parameters; } @CheckForNull public String getTemplateKey() { return templateKey; } public List getOverriddenImpacts() { return overridenImpacts; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/SecurityHotspotChangedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; import java.time.Instant; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; public class SecurityHotspotChangedEvent implements ServerHotspotEvent { private final String hotspotKey; private final String projectKey; private final Instant updateDate; private final HotspotReviewStatus status; private final String assignee; private final Path filePath; public SecurityHotspotChangedEvent(String hotspotKey, String projectKey, Instant updateDate, HotspotReviewStatus status, String assignee, Path filePath) { this.hotspotKey = hotspotKey; this.projectKey = projectKey; this.updateDate = updateDate; this.status = status; this.assignee = assignee; this.filePath = filePath; } public String getHotspotKey() { return hotspotKey; } @Override public String getProjectKey() { return projectKey; } public Instant getUpdateDate() { return updateDate; } public HotspotReviewStatus getStatus() { return status; } public String getAssignee() { return assignee; } @Override public Path getFilePath() { return filePath; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/SecurityHotspotClosedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; public class SecurityHotspotClosedEvent implements ServerHotspotEvent { private final String projectKey; private final String hotspotKey; private final Path filePath; public SecurityHotspotClosedEvent(String projectKey, String hotspotKey, Path filePath) { this.projectKey = projectKey; this.hotspotKey = hotspotKey; this.filePath = filePath; } @Override public String getProjectKey() { return projectKey; } public String getHotspotKey() { return hotspotKey; } @Override public Path getFilePath() { return filePath; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/SecurityHotspotRaisedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; import java.time.Instant; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; public class SecurityHotspotRaisedEvent implements ServerHotspotEvent { private final String hotspotKey; private final String projectKey; private final VulnerabilityProbability vulnerabilityProbability; private final HotspotReviewStatus status; private final Instant creationDate; private final String branch; private final TaintVulnerabilityRaisedEvent.Location mainLocation; private final String ruleKey; @Nullable private final String ruleDescriptionContextKey; @Nullable private final String assignee; public SecurityHotspotRaisedEvent(String hotspotKey, String projectKey, VulnerabilityProbability vulnerabilityProbability, HotspotReviewStatus status, Instant creationDate, String branch, TaintVulnerabilityRaisedEvent.Location mainLocation, String ruleKey, @Nullable String ruleDescriptionContextKey, @Nullable String assignee) { this.hotspotKey = hotspotKey; this.projectKey = projectKey; this.vulnerabilityProbability = vulnerabilityProbability; this.status = status; this.creationDate = creationDate; this.branch = branch; this.mainLocation = mainLocation; this.ruleKey = ruleKey; this.ruleDescriptionContextKey = ruleDescriptionContextKey; this.assignee = assignee; } public String getHotspotKey() { return hotspotKey; } @Override public String getProjectKey() { return projectKey; } public VulnerabilityProbability getVulnerabilityProbability() { return vulnerabilityProbability; } public HotspotReviewStatus getStatus() { return status; } public Instant getCreationDate() { return creationDate; } public String getBranch() { return branch; } public TaintVulnerabilityRaisedEvent.Location getMainLocation() { return mainLocation; } public String getRuleKey() { return ruleKey; } @Nullable public String getRuleDescriptionContextKey() { return ruleDescriptionContextKey; } @Override public Path getFilePath() { return mainLocation.getFilePath(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/ServerHotspotEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; public interface ServerHotspotEvent extends SonarProjectEvent { Path getFilePath(); } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/SonarProjectEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; public interface SonarProjectEvent extends SonarServerEvent { String getProjectKey(); } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/SonarServerEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; public interface SonarServerEvent { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/TaintVulnerabilityClosedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; public class TaintVulnerabilityClosedEvent implements SonarProjectEvent { private final String projectKey; private final String taintIssueKey; public TaintVulnerabilityClosedEvent(String projectKey, String taintIssueKey) { this.projectKey = projectKey; this.taintIssueKey = taintIssueKey; } @Override public String getProjectKey() { return projectKey; } public String getTaintIssueKey() { return taintIssueKey; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/TaintVulnerabilityRaisedEvent.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; public class TaintVulnerabilityRaisedEvent implements SonarProjectEvent { private final String key; private final String projectKey; private final String branchName; private final Instant creationDate; private final String ruleKey; private final IssueSeverity severity; private final RuleType type; private final Location mainLocation; private final List flows; @Nullable private final String ruleDescriptionContextKey; @Nullable private final CleanCodeAttribute cleanCodeAttribute; private final Map impacts; public TaintVulnerabilityRaisedEvent(String key, String projectKey, String branchName, Instant creationDate, String ruleKey, IssueSeverity severity, RuleType type, Location mainLocation, List flows, @Nullable String ruleDescriptionContextKey, @Nullable CleanCodeAttribute cleanCodeAttribute, Map impacts) { this.key = key; this.projectKey = projectKey; this.branchName = branchName; this.creationDate = creationDate; this.ruleKey = ruleKey; this.severity = severity; this.type = type; this.mainLocation = mainLocation; this.flows = flows; this.ruleDescriptionContextKey = ruleDescriptionContextKey; this.cleanCodeAttribute = cleanCodeAttribute; this.impacts = impacts; } public String getKey() { return key; } @Override public String getProjectKey() { return projectKey; } public String getBranchName() { return branchName; } public Instant getCreationDate() { return creationDate; } public String getRuleKey() { return ruleKey; } public IssueSeverity getSeverity() { return severity; } public RuleType getType() { return type; } public Location getMainLocation() { return mainLocation; } public List getFlows() { return flows; } public Optional getCleanCodeAttribute() { return Optional.ofNullable(cleanCodeAttribute); } public Map getImpacts() { return impacts; } @CheckForNull public String getRuleDescriptionContextKey() { return ruleDescriptionContextKey; } public static class Location { private final Path filePath; private final String message; private final TextRange textRange; public Location(Path filePath, String message, TextRange textRange) { this.filePath = filePath; this.message = message; this.textRange = textRange; } public Path getFilePath() { return filePath; } public String getMessage() { return message; } public TextRange getTextRange() { return textRange; } public static class TextRange { private final int startLine; private final int startLineOffset; private final int endLine; private final int endLineOffset; private final String hash; public TextRange(int startLine, int startLineOffset, int endLine, int endLineOffset, String hash) { this.startLine = startLine; this.startLineOffset = startLineOffset; this.endLine = endLine; this.endLineOffset = endLineOffset; this.hash = hash; } public int getStartLine() { return startLine; } public int getStartLineOffset() { return startLineOffset; } public int getEndLine() { return endLine; } public int getEndLineOffset() { return endLineOffset; } public String getHash() { return hash; } } } public static class Flow { private final List locations; public Flow(List locations) { this.locations = locations; } public List getLocations() { return locations; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.push; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/EventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import java.util.Optional; import org.sonarsource.sonarlint.core.serverapi.push.SonarServerEvent; public interface EventParser { Optional parse(String jsonData); } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/IssueChangedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.IssueChangedEvent; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class IssueChangedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, IssueChangedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'IssueChangedEvent' event: {}", jsonData); return Optional.empty(); } return Optional.of(new IssueChangedEvent( payload.projectKey, payload.issues.stream() .map(issueChange -> new IssueChangedEvent.Issue(issueChange.issueKey, issueChange.branchName, adapt(issueChange.impacts)) ) .toList(), payload.userSeverity != null ? IssueSeverity.valueOf(payload.userSeverity) : null, payload.userType != null ? RuleType.valueOf(payload.userType) : null, payload.resolved)); } public static Map adapt(@Nullable ImpactPayload[] payloads) { if (payloads == null) { return Map.of(); } return Arrays.stream(payloads) .collect(Collectors.toMap( payload -> SoftwareQuality.valueOf(payload.getSoftwareQuality()), payload -> ImpactSeverity.valueOf(payload.getSeverity()) )); } private static class IssueChangedEventPayload { private String projectKey; private List issues; private String userSeverity; private String userType; private Boolean resolved; private boolean isInvalid() { return isBlank(projectKey) || isBlank(issues) || issues.stream().anyMatch(ChangedIssuePayload::isInvalid) || (isBlank(userSeverity) && isBlank(userType) && resolved == null); } private static class ChangedIssuePayload { private String issueKey; private String branchName; @Nullable private ImpactPayload[] impacts; private boolean isInvalid() { return isBlank(issueKey) || isBlank(branchName); } } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/RuleSetChangedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.RuleSetChangedEvent; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.areBlank; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class RuleSetChangedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, RuleSetChangedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'RuleSetChanged' event: {}", jsonData); return Optional.empty(); } return Optional.of(new RuleSetChangedEvent( payload.projects, payload.activatedRules.stream().map(changedRule -> new RuleSetChangedEvent.ActiveRule( changedRule.key, changedRule.language, IssueSeverity.valueOf(changedRule.severity), changedRule.params.stream().filter(p -> p.value != null).collect(Collectors.toMap(p -> p.key, p -> p.value)), changedRule.templateKey, changedRule.impacts == null ? Collections.emptyList() : changedRule.impacts.stream() .map(impact -> new ImpactPayload(impact.getSoftwareQuality(), impact.getSeverity())).toList() )) .toList(), payload.deactivatedRules)); } private static class RuleSetChangedEventPayload { private List projects; private List activatedRules; private List deactivatedRules; private boolean isInvalid() { return isBlank(projects) || areBlank(activatedRules, deactivatedRules); } private static class ActiveRulePayload { private String key; private String language; private String severity; private List params; private String templateKey; private List impacts; } private static class RuleParameterPayload { private String key; private String value; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotChangedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.nio.file.Path; import java.time.Instant; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotChangedEvent; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class SecurityHotspotChangedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, HotspotChangedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'SecurityHotspotChanged' event: {}", jsonData); return Optional.empty(); } return Optional.of(new SecurityHotspotChangedEvent( payload.key, payload.projectKey, Instant.ofEpochMilli(payload.updateDate), HotspotReviewStatus.fromStatusAndResolution(payload.status, payload.resolution), payload.assignee, Path.of(payload.filePath))); } private static class HotspotChangedEventPayload { private String key; private String projectKey; private String status; private String resolution; private long updateDate; private String assignee; private String filePath; public String getKey() { return key; } public String getProjectKey() { return projectKey; } public String getStatus() { return status; } public String getResolution() { return resolution; } public long getUpdateDate() { return updateDate; } public String getAssignee() { return assignee; } private boolean isInvalid() { return isBlank(key) || isBlank(projectKey) || updateDate == 0L || isBlank(filePath); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotClosedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.nio.file.Path; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotClosedEvent; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class SecurityHotspotClosedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, HotspotClosedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'SecurityHotspotClosed' event: {}", jsonData); return Optional.empty(); } return Optional.of(new SecurityHotspotClosedEvent(payload.projectKey, payload.key, Path.of(payload.filePath))); } private static class HotspotClosedEventPayload { private String projectKey; private String key; private String filePath; private boolean isInvalid() { return isBlank(projectKey) || isBlank(key) || isBlank(filePath); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotRaisedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.time.Instant; import java.util.Optional; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.SecurityHotspotRaisedEvent; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.LocationPayload; import static org.sonarsource.sonarlint.core.serverapi.push.parsing.TaintVulnerabilityRaisedEventParser.adapt; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class SecurityHotspotRaisedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, HotspotRaisedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'SecurityHotspotRaised' event: {}", jsonData); return Optional.empty(); } return Optional.of(new SecurityHotspotRaisedEvent( payload.key, payload.projectKey, VulnerabilityProbability.valueOf(payload.vulnerabilityProbability), HotspotReviewStatus.fromStatusAndResolution(payload.status, payload.resolution), Instant.ofEpochMilli(payload.creationDate), payload.branch, adapt(payload.mainLocation), payload.ruleKey, payload.ruleDescriptionContextKey, payload.assignee)); } private static class HotspotRaisedEventPayload { private String key; private String projectKey; private String status; @Nullable private String resolution; private String branch; private String vulnerabilityProbability; private long creationDate; private String ruleKey; private LocationPayload mainLocation; @Nullable private String ruleDescriptionContextKey; @Nullable private String assignee; private boolean isInvalid() { return isBlank(key) || isBlank(projectKey) || isBlank(vulnerabilityProbability) || creationDate == 0L || isBlank(branch) || isBlank(ruleKey) || mainLocation.isInvalid(); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/TaintVulnerabilityClosedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.TaintVulnerabilityClosedEvent; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class TaintVulnerabilityClosedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, TaintVulnerabilityClosedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'TaintVulnerabilityClosed' event: {}", jsonData); return Optional.empty(); } return Optional.of(new TaintVulnerabilityClosedEvent(payload.projectKey, payload.key)); } private static class TaintVulnerabilityClosedEventPayload { private String projectKey; private String key; private boolean isInvalid() { return isBlank(projectKey) || isBlank(key); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/TaintVulnerabilityRaisedEventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import com.google.gson.Gson; import java.nio.file.Path; import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.TaintVulnerabilityRaisedEvent; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.LocationPayload; import static java.util.Objects.isNull; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class TaintVulnerabilityRaisedEventParser implements EventParser { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = new Gson(); @Override public Optional parse(String jsonData) { var payload = gson.fromJson(jsonData, TaintVulnerabilityRaisedEventPayload.class); if (payload.isInvalid()) { LOG.error("Invalid payload for 'TaintVulnerabilityRaised' event: {}", jsonData); return Optional.empty(); } return Optional.of(new TaintVulnerabilityRaisedEvent( payload.key, payload.projectKey, payload.branch, Instant.ofEpochMilli(payload.creationDate), payload.ruleKey, IssueSeverity.valueOf(payload.severity), RuleType.valueOf(payload.type), adapt(payload.mainLocation), adapt(payload.flows), payload.ruleDescriptionContextKey, adapt(payload.cleanCodeAttribute), adapt(payload.impacts) )); } private static List adapt(List flows) { return flows.stream() .map(f -> new TaintVulnerabilityRaisedEvent.Flow( f.locations.stream() .map(TaintVulnerabilityRaisedEventParser::adapt) .toList())) .toList(); } public static TaintVulnerabilityRaisedEvent.Location adapt(LocationPayload payload) { return new TaintVulnerabilityRaisedEvent.Location(Path.of(payload.getFilePath()), payload.getMessage(), adapt(payload.getTextRange())); } public static CleanCodeAttribute adapt(String cleanCodeAttribute) { return Optional.ofNullable(cleanCodeAttribute).map(CleanCodeAttribute::valueOf).orElse(null); } public static Map adapt(@Nullable ImpactPayload[] payloads) { if (payloads == null) { return Map.of(); } return Arrays.stream(payloads) .collect(Collectors.toMap( payload -> SoftwareQuality.valueOf(payload.getSoftwareQuality()), payload -> ImpactSeverity.valueOf(payload.getSeverity()) )); } private static TaintVulnerabilityRaisedEvent.Location.TextRange adapt(LocationPayload.TextRangePayload payload) { return new TaintVulnerabilityRaisedEvent.Location.TextRange(payload.getStartLine(), payload.getStartLineOffset(), payload.getEndLine(), payload.getEndLineOffset(), payload.getHash()); } private static class TaintVulnerabilityRaisedEventPayload { private String key; private String projectKey; private String branch; private long creationDate; private String ruleKey; private String severity; private String type; private LocationPayload mainLocation; private List flows; @Nullable private String ruleDescriptionContextKey; @Nullable private String cleanCodeAttribute; @Nullable private ImpactPayload[] impacts; private boolean isInvalid() { return isBlank(key) || isBlank(projectKey) || isBlank(branch) || creationDate == 0L || isBlank(ruleKey) || isBlank(severity) || isBlank(type) || isNull(mainLocation) || mainLocation.isInvalid() || isNull(flows) || flows.stream().anyMatch(FlowPayload::isInvalid); } private static class FlowPayload { private List locations; private boolean isInvalid() { return isNull(locations) || locations.stream().anyMatch(LocationPayload::isInvalid); } } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/common/ImpactPayload.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing.common; public class ImpactPayload { private String softwareQuality; private String severity; public ImpactPayload(String softwareQuality, String severity) { this.softwareQuality = softwareQuality; this.severity = severity; } public String getSoftwareQuality() { return softwareQuality; } public void setSoftwareQuality(String softwareQuality) { this.softwareQuality = softwareQuality; } public String getSeverity() { return severity; } public void setSeverity(String severity) { this.severity = severity; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/common/LocationPayload.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing.common; import static java.util.Objects.isNull; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.isBlank; public class LocationPayload { private String filePath; private String message; private TextRangePayload textRange; public String getFilePath() { return filePath; } public String getMessage() { return message; } public TextRangePayload getTextRange() { return textRange; } public boolean isInvalid() { return isBlank(filePath) || isBlank(message) || isNull(textRange) || textRange.isInvalid(); } public static class TextRangePayload { private int startLine; private int startLineOffset; private int endLine; private int endLineOffset; private String hash; public int getStartLine() { return startLine; } public int getStartLineOffset() { return startLineOffset; } public int getEndLine() { return endLine; } public int getEndLineOffset() { return endLineOffset; } public String getHash() { return hash; } public boolean isInvalid() { return isBlank(hash); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/common/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.push.parsing.common; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.push.parsing; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/qualityprofile/QualityProfile.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.qualityprofile; public class QualityProfile { private final boolean isDefault; private final String key; private final String name; private final String language; private final String languageName; private final long activeRuleCount; private final String rulesUpdatedAt; private final String userUpdatedAt; public QualityProfile(boolean isDefault, String key, String name, String language, String languageName, long activeRuleCount, String rulesUpdatedAt, String userUpdatedAt) { this.isDefault = isDefault; this.key = key; this.name = name; this.language = language; this.languageName = languageName; this.activeRuleCount = activeRuleCount; this.rulesUpdatedAt = rulesUpdatedAt; this.userUpdatedAt = userUpdatedAt; } public boolean isDefault() { return isDefault; } public String getKey() { return key; } public String getName() { return name; } public String getLanguage() { return language; } public String getLanguageName() { return languageName; } public long getActiveRuleCount() { return activeRuleCount; } public String getRulesUpdatedAt() { return rulesUpdatedAt; } public String getUserUpdatedAt() { return userUpdatedAt; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/qualityprofile/QualityProfileApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.qualityprofile; import java.util.List; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.ProjectNotFoundException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Qualityprofiles; public class QualityProfileApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String DEFAULT_QP_SEARCH_URL = "/api/qualityprofiles/search.protobuf"; private final ServerApiHelper helper; public QualityProfileApi(ServerApiHelper helper) { this.helper = helper; } public List getQualityProfiles(String projectKey, SonarLintCancelMonitor cancelMonitor) { Qualityprofiles.SearchWsResponse qpResponse; var url = new StringBuilder(); url.append(DEFAULT_QP_SEARCH_URL + "?project="); url.append(UrlUtils.urlEncode(projectKey)); helper.getOrganizationKey() .ifPresent(org -> url.append("&organization=").append(UrlUtils.urlEncode(org))); try { qpResponse = ServerApiHelper.processTimed( () -> helper.get(url.toString(), cancelMonitor), response -> Qualityprofiles.SearchWsResponse.parseFrom(response.bodyAsStream()), duration -> LOG.debug("Downloaded project quality profiles in {}ms", duration)); return qpResponse.getProfilesList().stream().map(QualityProfileApi::adapt).toList(); } catch (NotFoundException e) { throw new ProjectNotFoundException(projectKey, helper.getOrganizationKey().orElse(null)); } } private static QualityProfile adapt(Qualityprofiles.SearchWsResponse.QualityProfile wsQualityProfile) { return new QualityProfile( wsQualityProfile.getIsDefault(), wsQualityProfile.getKey(), wsQualityProfile.getName(), wsQualityProfile.getLanguage(), wsQualityProfile.getLanguageName(), wsQualityProfile.getActiveRuleCount(), wsQualityProfile.getRulesUpdatedAt(), wsQualityProfile.getUserUpdatedAt()); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/qualityprofile/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.qualityprofile; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/rules/RulesApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.rules; import com.google.common.base.Enums; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Rules; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; public class RulesApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final Map TAINT_REPOS_BY_LANGUAGE = Map.of( SonarLanguage.GO, "gosecurity", SonarLanguage.JAVA, "javasecurity", SonarLanguage.JS, "jssecurity", SonarLanguage.KOTLIN, "kotlinsecurity", SonarLanguage.PHP, "phpsecurity", SonarLanguage.PYTHON, "pythonsecurity", SonarLanguage.CS, "roslyn.sonaranalyzer.security.cs", SonarLanguage.TS, "tssecurity", SonarLanguage.VBNET, "vbnetsecurity"); public static final Set TAINT_REPOS = Set.copyOf(TAINT_REPOS_BY_LANGUAGE.values()); public static final String RULE_SHOW_URL = "/api/rules/show.protobuf?key="; private final ServerApiHelper serverApiHelper; public RulesApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } public Optional getRule(String ruleKey, SonarLintCancelMonitor cancelMonitor) { var builder = new StringBuilder(RULE_SHOW_URL + ruleKey); serverApiHelper.getOrganizationKey().ifPresent(org -> builder.append("&organization=").append(UrlUtils.urlEncode(org))); try (var response = serverApiHelper.get(builder.toString(), cancelMonitor)) { var rule = Rules.ShowResponse.parseFrom(response.bodyAsStream()).getRule(); var cleanCodeAttribute = Enums.getIfPresent(CleanCodeAttribute.class, rule.getCleanCodeAttribute().name()).orNull(); var impacts = rule.getImpacts().getImpactsList().stream().collect(toMap( impact -> SoftwareQuality.valueOf(impact.getSoftwareQuality().name()), impact -> ImpactSeverity.mapSeverity(impact.getSeverity().name()))); return Optional.of(new ServerRule(rule.getName(), IssueSeverity.valueOf(rule.getSeverity()), RuleType.valueOf(rule.getType().name()), rule.getLang(), rule.getHtmlDesc(), convertDescriptionSections(rule), rule.getHtmlNote(), Set.copyOf(rule.getEducationPrinciples().getEducationPrinciplesList()), cleanCodeAttribute, impacts)); } catch (Exception e) { LOG.error("Error when fetching rule '" + ruleKey + "'", e); } return Optional.empty(); } private static List convertDescriptionSections(Rules.Rule rule) { if (rule.hasDescriptionSections()) { return rule.getDescriptionSections().getDescriptionSectionsList().stream() .map(s -> { ServerRule.DescriptionSection.Context context = null; if (s.hasContext()) { var contextFromServer = s.getContext(); context = new ServerRule.DescriptionSection.Context(contextFromServer.getKey(), contextFromServer.getDisplayName()); } return new ServerRule.DescriptionSection(s.getKey(), s.getContent(), Optional.ofNullable(context)); }).toList(); } return Collections.emptyList(); } public Collection getAllActiveRules(String qualityProfileKey, SonarLintCancelMonitor cancelMonitor) { // Use a map to avoid duplicates during pagination Map activeRulesByKey = new HashMap<>(); Map ruleTemplatesByRuleKey = new HashMap<>(); serverApiHelper.getPaginated(getSearchByQualityProfileUrl(qualityProfileKey), Rules.SearchResponse::parseFrom, r -> r.hasPaging() ? r.getPaging().getTotal() : r.getTotal(), r -> { ruleTemplatesByRuleKey.putAll(r.getRulesList().stream().collect(toMap(Rules.Rule::getKey, Rules.Rule::getTemplateKey))); return List.copyOf(r.getActives().getActivesMap().entrySet()); }, activeEntry -> { var ruleKey = activeEntry.getKey(); // Since we are querying rules for a given profile, we know there will be only one active rule per rule Rules.Active ar = activeEntry.getValue().getActiveListList().get(0); activeRulesByKey.put(ruleKey, new ServerActiveRule( ruleKey, IssueSeverity.valueOf(ar.getSeverity()), ar.getParamsList().stream().collect(toMap(Rules.Active.Param::getKey, Rules.Active.Param::getValue)), ruleTemplatesByRuleKey.get(ruleKey), ar.getImpacts().getImpactsList().stream() .map(impact -> new ImpactPayload(impact.getSoftwareQuality().toString(), ImpactSeverity.mapSeverity(impact.getSeverity().name()).name())) .toList())); }, false, cancelMonitor); return activeRulesByKey.values(); } private String getSearchByQualityProfileUrl(String qualityProfileKey) { var builder = new StringBuilder(); builder.append("/api/rules/search.protobuf?qprofile="); builder.append(UrlUtils.urlEncode(qualityProfileKey)); serverApiHelper.getOrganizationKey().ifPresent(org -> builder.append("&organization=").append(UrlUtils.urlEncode(org))); builder.append("&activation=true&f=templateKey,actives&types=CODE_SMELL,BUG,VULNERABILITY,SECURITY_HOTSPOT&s=key"); return builder.toString(); } public Set getAllTaintRules(List enabledLanguages, SonarLintCancelMonitor cancelMonitor) { Set taintRules = new HashSet<>(); serverApiHelper.getPaginated(getSearchByRepoUrl(enabledLanguages.stream().map(TAINT_REPOS_BY_LANGUAGE::get).filter(Objects::nonNull).toList()), Rules.SearchResponse::parseFrom, Rules.SearchResponse::getTotal, Rules.SearchResponse::getRulesList, rule -> taintRules.add(rule.getKey()), false, cancelMonitor); return taintRules; } private String getSearchByRepoUrl(List repositories) { var builder = new StringBuilder(); builder.append("/api/rules/search.protobuf?repositories="); builder.append(repositories.stream().map(UrlUtils::urlEncode).collect(joining(","))); serverApiHelper.getOrganizationKey().ifPresent(org -> builder.append("&organization=").append(UrlUtils.urlEncode(org))); // Add only f=repo even if we don't need it, else too many fields are returned by default builder.append("&f=repo&s=key"); return builder.toString(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/rules/ServerActiveRule.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.rules; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; public class ServerActiveRule { private final String ruleKey; private final IssueSeverity severity; private final Map params; private final String templateKey; private final List overriddenImpacts; public ServerActiveRule(String ruleKey, IssueSeverity severity, Map params, @Nullable String templateKey, List overriddenImpacts) { this.ruleKey = ruleKey; this.severity = severity; this.params = params; this.templateKey = templateKey; this.overriddenImpacts = overriddenImpacts; } public List getOverriddenImpacts() { return overriddenImpacts; } public IssueSeverity getSeverity() { return severity; } public Map getParams() { return params; } public String getRuleKey() { return ruleKey; } public String getTemplateKey() { return templateKey; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/rules/ServerRule.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.rules; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class ServerRule { private final String name; private final String htmlDesc; private final List descriptionSections; private final String htmlNote; private final IssueSeverity severity; private final RuleType type; private final SonarLanguage language; private final Set educationPrincipleKeys; private final CleanCodeAttribute cleanCodeAttribute; private final Map impacts; public ServerRule(String name, IssueSeverity severity, RuleType type, String language, String htmlDesc, List descriptionSections, String htmlNote, Set educationPrincipleKeys, @Nullable CleanCodeAttribute cleanCodeAttribute, Map impacts) { this.name = name; this.severity = severity; this.type = type; this.language = SonarLanguage.forKey(language).orElseThrow(() -> new IllegalArgumentException("Unknown language with key: " + language)); this.htmlDesc = htmlDesc; this.descriptionSections = descriptionSections; this.htmlNote = htmlNote; this.educationPrincipleKeys = educationPrincipleKeys; this.cleanCodeAttribute = cleanCodeAttribute; this.impacts = impacts; } public String getName() { return name; } public String getHtmlDesc() { return htmlDesc; } public List getDescriptionSections() { return descriptionSections; } public String getHtmlNote() { return htmlNote; } public IssueSeverity getSeverity() { return severity; } public RuleType getType() { return type; } public SonarLanguage getLanguage() { return language; } public Set getEducationPrincipleKeys() { return educationPrincipleKeys; } @CheckForNull public CleanCodeAttribute getCleanCodeAttribute() { return cleanCodeAttribute; } public Map getImpacts() { return impacts; } public static class DescriptionSection { private final String key; private final String htmlContent; private final Optional context; public DescriptionSection(String key, String htmlContent, Optional context) { this.key = key; this.htmlContent = htmlContent; this.context = context; } public String getKey() { return key; } public String getHtmlContent() { return htmlContent; } public Optional getContext() { return context; } public static class Context { private final String key; private final String displayName; public Context(String key, String displayName) { this.key = key; this.displayName = displayName; } public String getKey() { return key; } public String getDisplayName() { return displayName; } } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/rules/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.rules; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/sca/GetIssuesReleasesResponse.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.sca; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; public record GetIssuesReleasesResponse(List issuesReleases, Page page) { public record IssuesRelease(UUID key, Type type, Severity severity, SoftwareQuality quality, Status status, Release release, @Nullable String vulnerabilityId, @Nullable String cvssScore, List transitions) { public record Release(String packageName, String version) { } public enum Severity { INFO, LOW, MEDIUM, HIGH, BLOCKER } public enum SoftwareQuality { MAINTAINABILITY, RELIABILITY, SECURITY } public enum Type { VULNERABILITY, PROHIBITED_LICENSE } public enum Status { OPEN, CONFIRM, ACCEPT, SAFE, FIXED } public enum Transition { CONFIRM, REOPEN, SAFE, FIXED, ACCEPT } } public record Page(int total) { } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/sca/GetScaEnablementResponse.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.sca; public record GetScaEnablementResponse(boolean enabled) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/sca/ScaApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.sca; import com.google.gson.Gson; import jakarta.annotation.Nullable; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.UUID; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; public class ScaApi { private final ServerApiHelper serverApiHelper; public ScaApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } public GetIssuesReleasesResponse getIssuesReleases(String projectKey, String branchKey, SonarLintCancelMonitor cancelMonitor) { var urlPrefix = serverApiHelper.isSonarCloud() ? "" : "/api/v2"; var url = urlPrefix + "/sca/issues-releases?projectKey=" + UrlUtils.urlEncode(projectKey) + "&branchKey=" + UrlUtils.urlEncode(branchKey); var allIssuesReleases = new ArrayList(); if (serverApiHelper.isSonarCloud()) { serverApiHelper.apiGetPaginated( url, response -> new Gson().fromJson(new InputStreamReader(response, StandardCharsets.UTF_8), GetIssuesReleasesResponse.class), r -> r.page().total(), GetIssuesReleasesResponse::issuesReleases, allIssuesReleases::add, false, cancelMonitor, "pageIndex", "pageSize"); } else { serverApiHelper.getPaginated( url, response -> new Gson().fromJson(new InputStreamReader(response, StandardCharsets.UTF_8), GetIssuesReleasesResponse.class), r -> r.page().total(), GetIssuesReleasesResponse::issuesReleases, allIssuesReleases::add, false, cancelMonitor, "pageIndex", "pageSize"); } return new GetIssuesReleasesResponse(allIssuesReleases, new GetIssuesReleasesResponse.Page(allIssuesReleases.size())); } public void changeStatus(UUID issueReleaseKey, String transitionKey, @Nullable String comment, SonarLintCancelMonitor cancelMonitor) { var body = new ChangeStatusRequestBody(issueReleaseKey.toString(), transitionKey, comment); var urlPrefix = serverApiHelper.isSonarCloud() ? "" : "/api/v2"; var url = urlPrefix + "/sca/issues-releases/change-status"; if (serverApiHelper.isSonarCloud()) { serverApiHelper.apiPostJson(url, body, cancelMonitor); } else { serverApiHelper.postJson(url, body, cancelMonitor); } } private record ChangeStatusRequestBody(String issueReleaseKey, String transitionKey, @Nullable String comment) { } public GetScaEnablementResponse isScaEnabled(SonarLintCancelMonitor cancelMonitor) { var organizationKey = serverApiHelper.getOrganizationKey(); if (organizationKey.isEmpty()) { return new GetScaEnablementResponse(false); } try { return serverApiHelper.apiGetJson("/sca/feature-enabled?organization=" + UrlUtils.urlEncode(organizationKey.get()), GetScaEnablementResponse.class, cancelMonitor); } catch (Exception e) { return new GetScaEnablementResponse(false); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/sca/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.sca; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/settings/SettingsApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.settings; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.function.BiConsumer; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Settings; public class SettingsApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final String API_SETTINGS_PATH = "/api/settings/values.protobuf"; private final ServerApiHelper helper; public SettingsApi(ServerApiHelper helper) { this.helper = helper; } public Map getGlobalSettings(SonarLintCancelMonitor cancelMonitor) { return getSettings("", cancelMonitor); } public Map getProjectSettings(String projectKey, SonarLintCancelMonitor cancelMonitor) { return getSettings("?component=" + UrlUtils.urlEncode(projectKey), cancelMonitor); } private Map getSettings(String queryParameters, SonarLintCancelMonitor cancelMonitor) { var settings = new HashMap(); var url = API_SETTINGS_PATH + queryParameters; ServerApiHelper.consumeTimed( () -> helper.get(url, cancelMonitor), response -> { try (var is = response.bodyAsStream()) { var values = Settings.ValuesWsResponse.parseFrom(is); for (Settings.Setting s : values.getSettingsList()) { processSetting(settings::put, s); } } catch (IOException e) { throw new IllegalStateException("Unable to parse properties from: " + response.bodyAsString(), e); } }, duration -> LOG.info("Downloaded settings in {}ms", duration)); return settings; } private static void processSetting(BiConsumer consumer, Settings.Setting s) { switch (s.getValueOneOfCase()) { case VALUE: consumer.accept(s.getKey(), s.getValue()); break; case VALUES: consumer.accept(s.getKey(), String.join(",", s.getValues().getValuesList())); break; case FIELDVALUES: processPropertySet(s, consumer); break; default: throw new IllegalStateException("Unknown property value for " + s.getKey()); } } private static void processPropertySet(Settings.Setting s, BiConsumer consumer) { var ids = new ArrayList(); var id = 1; for (Settings.FieldValues.Value v : s.getFieldValues().getFieldValuesList()) { for (Map.Entry entry : v.getValueMap().entrySet()) { consumer.accept(s.getKey() + "." + id + "." + entry.getKey(), entry.getValue()); } ids.add(String.valueOf(id)); id++; } consumer.accept(s.getKey(), String.join(",", ids)); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/settings/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.settings; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/source/SourceApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.source; import java.util.Optional; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static org.sonarsource.sonarlint.core.serverapi.UrlUtils.urlEncode; public class SourceApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper serverApiHelper; public SourceApi(ServerApiHelper serverApiHelper) { this.serverApiHelper = serverApiHelper; } /** * Fetch source code of the file with specified key. * If the component doesn't exist or it exists but has no source, an empty String is returned. * * @param key project key, or file key. */ public Optional getRawSourceCode(String fileKey, SonarLintCancelMonitor cancelMonitor) { try (var r = serverApiHelper.get("/api/sources/raw?key=" + urlEncode(fileKey), cancelMonitor)) { return Optional.of(r.bodyAsString()); } catch (Exception e) { LOG.debug("Unable to fetch source code of '" + fileKey + "'", e); return Optional.empty(); } } public Optional getRawSourceCodeForBranchAndPullRequest(String fileKey, String branch, @Nullable String pullRequest, SonarLintCancelMonitor cancelMonitor) { var url = "/api/sources/raw?key=" + urlEncode(fileKey); if (pullRequest != null && !pullRequest.isEmpty()) { url = url.concat("&pullRequest=").concat(urlEncode(pullRequest)); } else if (!branch.isEmpty()) { url = url.concat("&branch=").concat(urlEncode(branch)); } try (var r = serverApiHelper.get(url, cancelMonitor)) { return Optional.of(r.bodyAsString()); } catch (Exception e) { LOG.debug("Unable to fetch source code of '" + fileKey + "'", e); return Optional.empty(); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/source/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.source; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/Event.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.stream; public class Event { private final String type; private final String data; public Event(String type, String data) { this.type = type; this.data = data; } public String getType() { return type; } public String getData() { return data; } @Override public String toString() { return "[type: " + type + ", " + "data: " + data + "]"; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventBuffer.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.stream; import java.util.ArrayList; import java.util.List; public class EventBuffer { private final StringBuilder buffer = new StringBuilder(); EventBuffer append(String data) { buffer.append(data); return this; } List drainCompleteEvents() { List completeEvents = new ArrayList<>(); int firstEventEndIndex; do { firstEventEndIndex = buffer.indexOf("\n\n"); if (firstEventEndIndex == -1) { break; } var completeEvent = buffer.substring(0, firstEventEndIndex).trim(); buffer.delete(0, firstEventEndIndex + 2); if (!completeEvent.isEmpty()) { completeEvents.add(completeEvent); } } while (true); return completeEvents; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventParser.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.stream; import java.util.List; public class EventParser { private static final String EVENT_TYPE_PREFIX = "event: "; private static final String DATA_PREFIX = "data: "; static Event parse(String eventPayload) { var fields = List.of(eventPayload.split("\\n")); var type = ""; var data = new StringBuilder(); for (String field : fields) { if (field.startsWith(EVENT_TYPE_PREFIX)) { type = field.substring(EVENT_TYPE_PREFIX.length()); } else if (field.startsWith(DATA_PREFIX)) { data.append(field.substring(DATA_PREFIX.length())); } } return new Event(type, data.toString()); } private EventParser() { // static only } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/EventStream.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.stream; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.util.FailSafeExecutors; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpConnectionListener; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static java.util.concurrent.TimeUnit.SECONDS; public class EventStream { private static final SonarLintLogger LOG = SonarLintLogger.get(); private static final Integer UNAUTHORIZED = 401; private static final Integer FORBIDDEN = 403; private static final Integer NOT_FOUND = 404; private static final long HEART_BEAT_PERIOD = 60; private final ServerApiHelper helper; private final ScheduledExecutorService executor; private final AtomicReference currentRequest = new AtomicReference<>(); private final AtomicReference> pendingFuture = new AtomicReference<>(); private final Consumer eventConsumer; public EventStream(ServerApiHelper helper, Consumer eventConsumer) { this(helper, eventConsumer, FailSafeExecutors.newSingleThreadScheduledExecutor("sonarlint-event-stream-consumer")); } EventStream(ServerApiHelper helper, Consumer eventConsumer, ScheduledExecutorService executor) { this.helper = helper; this.eventConsumer = eventConsumer; this.executor = executor; } public EventStream connect(String wsPath) { return connect(wsPath, new Attempt()); } private EventStream connect(String wsPath, Attempt currentAttempt) { LOG.debug("Connecting to server event-stream at '" + wsPath + "'..."); var eventBuffer = new EventBuffer(); currentRequest.set(helper.getEventStream(wsPath, new HttpConnectionListener() { @Override public void onConnected() { LOG.debug("Connected to server event-stream"); schedule(() -> connect(wsPath), HEART_BEAT_PERIOD * 3); } @Override public void onError(@Nullable Integer responseCode) { handleError(wsPath, currentAttempt, responseCode); } @Override public void onClosed() { cancelPendingFutureIfAny(); // reconnect instantly (will also reset attempt parameters) LOG.debug("Disconnected from server event-stream, reconnecting now"); connect(wsPath); } }, message -> { cancelPendingFutureIfAny(); eventBuffer.append(message) .drainCompleteEvents() .forEach(stringEvent -> { LOG.debug("Received event: " + stringEvent); eventConsumer.accept(EventParser.parse(stringEvent)); }); })); return this; } private void handleError(String wsPath, Attempt currentAttempt, @Nullable Integer responseCode) { if (shouldRetry(responseCode)) { if (!currentAttempt.isMax()) { var retryDelay = currentAttempt.delay; var msgBuilder = new StringBuilder(); msgBuilder.append("Cannot connect to server event-stream"); if (responseCode != null) { msgBuilder.append(" (").append(responseCode).append(")"); } msgBuilder.append(", retrying in ").append(retryDelay).append("s"); LOG.debug(msgBuilder.toString()); schedule(() -> connect(wsPath, currentAttempt.next()), retryDelay); } else { LOG.debug("Cannot connect to server event-stream, stop retrying"); } } } private static boolean shouldRetry(@Nullable Integer responseCode) { if (UNAUTHORIZED.equals(responseCode)) { LOG.debug("Cannot connect to server event-stream, unauthorized"); return false; } if (FORBIDDEN.equals(responseCode)) { LOG.debug("Cannot connect to server event-stream, forbidden"); return false; } if (NOT_FOUND.equals(responseCode)) { // the API is not supported (probably an old SQ or SC) LOG.debug("Server events not supported by the server"); return false; } return true; } private void schedule(Runnable task, long delayInSeconds) { if (!executor.isShutdown()) { pendingFuture.set(executor.schedule(task, delayInSeconds, SECONDS)); } } public void close() { cancelPendingFutureIfAny(); var currentRequestOrNull = currentRequest.getAndSet(null); if (currentRequestOrNull != null) { currentRequestOrNull.cancel(); } if (!MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS)) { LOG.warn("Unable to stop event stream executor service in a timely manner"); } } private void cancelPendingFutureIfAny() { var pendingFutureOrNull = pendingFuture.getAndSet(null); if (pendingFutureOrNull != null) { pendingFutureOrNull.cancel(true); } } private static class Attempt { private static final int DEFAULT_DELAY_S = 60; private static final int BACK_OFF_MULTIPLIER = 2; private static final int MAX_ATTEMPTS = 10; private final long delay; private final int attemptNumber; public Attempt() { this(DEFAULT_DELAY_S, 1); } public Attempt(long delay, int attemptNumber) { this.delay = delay; this.attemptNumber = attemptNumber; } public Attempt next() { return new Attempt(delay * BACK_OFF_MULTIPLIER, attemptNumber + 1); } public boolean isMax() { return attemptNumber == MAX_ATTEMPTS; } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/stream/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.stream; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/system/ServerStatusInfo.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.system; public record ServerStatusInfo(String id, String status, String version) { public boolean isUp() { return "UP".equals(status); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/system/SystemApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.system; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; public class SystemApi { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ServerApiHelper helper; public SystemApi(ServerApiHelper helper) { this.helper = helper; } public ServerStatusInfo getStatus(SonarLintCancelMonitor cancelMonitor) { var start = System.currentTimeMillis(); var status = helper.getAnonymousJson("api/system/status", SystemStatusDto.class, cancelMonitor); var duration = System.currentTimeMillis() - start; LOG.debug("Downloaded server infos in {}ms", duration); return new ServerStatusInfo(status.id(), status.status(), status.version()); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/system/SystemStatusDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.system; public record SystemStatusDto(String id, String version, String status) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/system/ValidationResult.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.system; public class ValidationResult { private final boolean success; private final String message; public ValidationResult(boolean success, String message) { this.success = success; this.message = message; } public boolean success() { return success; } public String message() { return message; } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/system/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.system; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/users/CurrentUserResponseDto.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.users; public record CurrentUserResponseDto(String id) { } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/users/UsersApi.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.users; import javax.annotation.CheckForNull; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; public class UsersApi { private final ServerApiHelper helper; public UsersApi(ServerApiHelper helper) { this.helper = helper; } /** * Fetch the current user info using api/users/current. * Returns null if the response cannot be parsed or if the id field is not present. * Note: The id field is available on SonarQube Cloud and SonarQube Server 2025.6+. */ @CheckForNull public String getCurrentUserId(SonarLintCancelMonitor cancelMonitor) { var userResponse = helper.getJson("/api/users/current", CurrentUserResponseDto.class, cancelMonitor); return userResponse == null ? null : userResponse.id(); } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/users/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.users; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/util/ProtobufUtil.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.util; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import com.google.protobuf.Parser; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; public class ProtobufUtil { private ProtobufUtil() { // only static stuff } public static List readMessages(InputStream input, Parser parser) { List list = new ArrayList<>(); while (true) { T message; try { message = parser.parseDelimitedFrom(input); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("failed to parse protobuf message", e); } if (message == null) { break; } list.add(message); } return list; } public static void writeMessages(OutputStream output, Iterable messages) { for (Message message : messages) { writeMessage(output, message); } } static void writeMessage(OutputStream output, T message) { try { message.writeDelimitedTo(output); } catch (IOException e) { throw new IllegalStateException("failed to write message: " + message, e); } } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/util/ServerApiUtils.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.util; import java.io.File; import java.nio.file.Path; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.TextRange; public class ServerApiUtils { public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); public static String extractCodeSnippet(String sourceCode, TextRange textRange) { return extractCodeSnippet(sourceCode.split("\\r?\\n"), textRange); } private static String extractCodeSnippet(String[] sourceCodeLines, TextRange textRange) { if (textRange.getStartLine() == 0 && textRange.getEndLine() == 0) { // SLCORE-593 this is a file-level issue return String.join("\n", sourceCodeLines); } else if (textRange.getStartLine() == textRange.getEndLine()) { var fullLine = sourceCodeLines[textRange.getStartLine() - 1]; return fullLine.substring(textRange.getStartOffset(), textRange.getEndOffset()); } else { var linesOfTextRange = Arrays.copyOfRange(sourceCodeLines, textRange.getStartLine() - 1, textRange.getEndLine()); linesOfTextRange[0] = linesOfTextRange[0].substring(textRange.getStartOffset()); linesOfTextRange[linesOfTextRange.length - 1] = linesOfTextRange[linesOfTextRange.length - 1].substring(0, textRange.getEndOffset()); return String.join("\n", linesOfTextRange); } } public static boolean isBlank(@Nullable Collection collection) { return collection == null || collection.isEmpty(); } public static boolean isBlank(@Nullable String s) { return s == null || s.isEmpty(); } public static boolean areBlank(List... lists) { return Arrays.stream(lists).allMatch(ServerApiUtils::isBlank); } public static OffsetDateTime parseOffsetDateTime(String s) { try { return OffsetDateTime.parse(s, DATETIME_FORMATTER); } catch (DateTimeParseException e) { throw new IllegalStateException("The date '" + s + "' does not respect format '" + DATETIME_FORMAT + "'", e); } } /** * Converts path to format used by SonarQube * * @param path path string in the local OS * @return SonarQube path */ public static String toSonarQubePath(Path path) { var pathAsString = path.toString(); var sonarQubeSeparatorChar = '/'; if (File.separatorChar != sonarQubeSeparatorChar) { return pathAsString.replaceAll(Pattern.quote(File.separator), String.valueOf(sonarQubeSeparatorChar)); } return pathAsString; } private ServerApiUtils() { // utility class } } ================================================ FILE: backend/server-api/src/main/java/org/sonarsource/sonarlint/core/serverapi/util/package-info.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverapi.util; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-api/src/main/proto/sonarcloud/ws-organizations.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarcloud.ws.organizations; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws"; option java_outer_classname = "Organizations"; option optimize_for = SPEED; // WS api/organizations/search message SearchWsResponse { repeated Organization organizations = 1; optional sonarqube.ws.commons.Paging paging = 2; } message Organization { optional string key = 1; optional string name = 2; optional string description = 3; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-commons.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.commons; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Common"; option optimize_for = SPEED; message Paging { optional int32 pageIndex = 1; optional int32 pageSize = 2; optional int32 total = 3; } enum Severity { INFO = 0; MINOR = 1; MAJOR = 2; CRITICAL = 3; BLOCKER = 4; } message Rule { optional string key = 1; optional string name = 2; optional string lang = 3; optional string langName = 5; } message Rules { repeated Rule rules = 1; } enum CleanCodeAttribute { UNKNOWN_ATTRIBUTE = 0; CONVENTIONAL = 1; FORMATTED = 2; IDENTIFIABLE = 3; CLEAR = 4; COMPLETE = 5; EFFICIENT = 6; LOGICAL = 7; DISTINCT = 8; FOCUSED = 9; MODULAR = 10; TESTED = 11; LAWFUL = 12; RESPECTFUL = 13; TRUSTWORTHY = 14; } enum CleanCodeAttributeCategory { UNKNOWN_CATEGORY = 0; ADAPTABLE = 1; CONSISTENT = 2; INTENTIONAL = 3; RESPONSIBLE = 4; } message Impact { required SoftwareQuality softwareQuality = 1; required ImpactSeverity severity = 2; } enum SoftwareQuality { UNKNOWN_IMPACT_QUALITY = 0; MAINTAINABILITY = 1; RELIABILITY = 2; SECURITY = 3; } enum ImpactSeverity { UNKNOWN_IMPACT_SEVERITY = 0; LOW = 1; MEDIUM = 2; HIGH = 3; // INFO and BLOCKER conflicts with Severity enum, so we use different values prefixed with enum name ImpactSeverity_INFO = 4; ImpactSeverity_BLOCKER = 5; } // Lines start at 1 and line offsets start at 0 message TextRange { // Start line. Should never be absent optional int32 startLine = 1; // End line (inclusive). Absent means it is same as start line optional int32 endLine = 2; // If absent it means range starts at the first offset of start line optional int32 startOffset = 3; // If absent it means range ends at the last offset of end line optional int32 endOffset = 4; } message Flow { repeated Location locations = 1; } message Location { optional string component = 4; optional string unusedComponentId = 1; // Only when component is a file. Can be empty for a file if this is an issue global to the file. optional sonarqube.ws.commons.TextRange textRange = 2; optional string msg = 3; } enum RuleType { // Zero is required in order to not get CODE_SMELL as default value // See http://androiddevblog.com/protocol-buffers-pitfall-adding-enum-values/ UNKNOWN = 0; // same name as in Java enum IssueType, // same index values as in database (see column ISSUES.ISSUE_TYPE) CODE_SMELL = 1; BUG = 2; VULNERABILITY = 3; SECURITY_HOTSPOT = 4; } enum BranchType { UNKNOWN_BRANCH_TYPE = 0; LONG = 1; SHORT = 2; PULL_REQUEST = 3; BRANCH = 4; } message Metric { optional string key = 1; optional string name = 2; optional string description = 3; optional string domain = 4; optional string type = 5; optional bool higherValuesAreBetter = 6; optional bool qualitative = 7; optional bool hidden = 8; optional bool custom = 9; optional int32 decimalScale = 10; optional string bestValue = 11; optional string worstValue = 12; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-components.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.component; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Components"; option optimize_for = SPEED; // WS api/components/search message SearchWsResponse { optional sonarqube.ws.commons.Paging paging = 1; repeated Component components = 2; } // WS api/components/tree message TreeWsResponse { optional sonarqube.ws.commons.Paging paging = 1; optional Component baseComponent = 3; repeated Component components = 4; } // WS api/components/show message ShowWsResponse { optional sonarqube.ws.commons.Paging paging = 1; optional Component component = 2; repeated Component ancestors = 3; } message Component { optional string key = 2; optional string name = 6; optional bool isAiCodeFixEnabled = 23; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-hotspots.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.hotspots; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Hotspots"; option optimize_for = SPEED; // Response of GET api/hotspots/search message SearchWsResponse { optional sonarqube.ws.commons.Paging paging = 1; repeated Hotspot hotspots = 2; repeated Component components = 3; message Hotspot { optional string key = 1; optional string component = 2; optional string project = 3; optional string securityCategory = 4; optional string vulnerabilityProbability = 5; optional string status = 6; optional string resolution = 7; optional int32 line = 8; optional string message = 9; optional string assignee = 10; optional string author = 11; optional string creationDate = 12; optional string updateDate = 13; optional sonarqube.ws.commons.TextRange textRange = 14; repeated sonarqube.ws.commons.Flow flows = 15; optional string ruleKey = 16; } } // Response of GET api/hotspots/show message ShowWsResponse { optional string key = 1; optional Component component = 2; optional Component project = 3; optional Rule rule = 4; optional string status = 5; optional string resolution = 6; optional string message = 8; optional string author = 10; optional sonarqube.ws.commons.TextRange textRange = 13; optional bool canChangeStatus = 17; } message Component { optional string key = 2; optional string path = 6; } message Rule { optional string key = 1; optional string name = 2; optional string securityCategory = 3; optional string vulnerabilityProbability = 4; optional string riskDescription = 5 [deprecated=true]; optional string vulnerabilityDescription = 6 [deprecated=true]; optional string fixRecommendations = 7 [deprecated=true]; } // Response of GET api/hotspots/pull message HotspotPullQueryTimestamp { required int64 queryTimestamp = 1; } message HotspotLite { optional string key = 1; optional string filePath = 2; optional string vulnerabilityProbability = 3; optional string status = 4; optional string resolution = 5; optional string message = 6; optional int64 creationDate = 7; optional TextRange textRange = 8; optional string ruleKey = 9; optional bool closed = 10; optional string assignee = 11; } message TextRange { optional int32 startLine = 1; optional int32 startLineOffset = 2; optional int32 endLine = 3; optional int32 endLineOffset = 4; optional string hash = 5; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-issues.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.issues; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Issues"; option optimize_for = SPEED; // Response of GET api/issues/search message SearchWsResponse { optional sonarqube.ws.commons.Paging paging = 4; repeated Issue issues = 6; repeated Component components = 7; optional sonarqube.ws.commons.Rules rules = 8; } message Issue { optional string key = 1; optional string rule = 2; optional sonarqube.ws.commons.Severity severity = 3; optional string component = 4; optional int32 line = 8; optional string hash = 31; optional sonarqube.ws.commons.TextRange textRange = 9; repeated sonarqube.ws.commons.Flow flows = 10; optional string resolution = 11; optional string status = 12; optional string message = 13; optional string assignee = 15; // the transitions allowed for the requesting user. optional Transitions transitions = 20; optional string creationDate = 23; optional sonarqube.ws.commons.RuleType type = 27; optional string ruleDescriptionContextKey = 37; // skipping unused fields 38-39 optional sonarqube.ws.commons.CleanCodeAttribute cleanCodeAttribute = 40; // skipping unused field cleanCodeAttributeCategory = 41; repeated sonarqube.ws.commons.Impact impacts = 42; } message Transitions { repeated string transitions = 1; } message Component { optional string key = 2; optional string path = 8; } // Response of GET api/issues/pull message IssuesPullQueryTimestamp { required int64 queryTimestamp = 1; } message TextRange { optional int32 startLine = 1; optional int32 startLineOffset = 2; optional int32 endLine = 3; optional int32 endLineOffset = 4; optional string hash = 5; } message Location { optional string filePath = 1; optional string message = 2; optional TextRange textRange = 3; } message IssueLite { required string key = 1; optional int64 creationDate = 2; optional bool resolved = 3; optional string ruleKey = 4; optional sonarqube.ws.commons.Severity userSeverity = 5; optional sonarqube.ws.commons.RuleType type = 6; optional Location mainLocation = 7; optional bool closed = 8; repeated sonarqube.ws.commons.Impact impacts = 9; } // Response of GET api/issues/pull_taint message TaintVulnerabilityPullQueryTimestamp { required int64 queryTimestamp = 1; } message TaintVulnerabilityLite { required string key = 1; optional int64 creationDate = 2; optional bool resolved = 3; optional string ruleKey = 4; optional sonarqube.ws.commons.Severity severity = 5; optional sonarqube.ws.commons.RuleType type = 6; optional Location mainLocation = 7; optional bool closed = 8; repeated Flow flows = 9; optional bool assignedToSubscribedUser = 10; optional string ruleDescriptionContextKey = 11; optional sonarqube.ws.commons.CleanCodeAttribute cleanCodeAttribute = 12; // skipping unused field cleanCodeAttributeCategory = 13; repeated sonarqube.ws.commons.Impact impacts = 14; } message Flow { repeated Location locations = 1; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-measures.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.measures; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Measures"; option optimize_for = SPEED; // WS api/measures/component message ComponentWsResponse { optional Component component = 1; optional Metrics metrics = 2; optional Periods periods = 3; optional Period period = 4; } message Component { reserved 1,3; optional string key = 2; optional string refKey = 4; optional string projectId = 5; optional string name = 6; optional string description = 7; optional string qualifier = 8; optional string path = 9; optional string language = 10; repeated Measure measures = 11; optional string branch = 12; optional string pullRequest = 13; } message Period { //deprecated since 8.1 optional int32 index = 1; optional string mode = 2; optional string date = 3; optional string parameter = 4; } message Periods { repeated Period periods = 1; } message Metrics { repeated sonarqube.ws.commons.Metric metrics = 1; } message Measure { optional string metric = 1; optional string value = 2; reserved 3; // periods optional string component = 4; optional bool bestValue = 5; optional PeriodValue period = 6; } message PeriodValue { //deprecated since 8.1 optional int32 index = 1; optional string value = 2; optional bool bestValue = 3; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-projectbranches.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.projectbranch; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "ProjectBranches"; option optimize_for = SPEED; import "sonarqube/ws-commons.proto"; // WS api/project_branches/list message ListWsResponse { repeated Branch branches = 1; } message Branch { optional string name = 1; optional bool isMain = 2; optional sonarqube.ws.commons.BranchType type = 3; } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-qualityprofiles.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.qualityprofiles; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Qualityprofiles"; option optimize_for = SPEED; // WS api/qualityprofiles/search message SearchWsResponse { repeated QualityProfile profiles = 1; message QualityProfile { optional string key = 1; optional string name = 2; optional string language = 3; optional string languageName = 4; optional bool isDefault = 8; optional int64 activeRuleCount = 9; optional string rulesUpdatedAt = 11; optional string userUpdatedAt = 14; } } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-rules.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto2"; package sonarqube.ws.rules; import "sonarqube/ws-commons.proto"; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Rules"; option optimize_for = SPEED; // WS api/rules/search message SearchResponse { optional int64 total = 1 [deprecated = true]; // Deprecated since 9.8 optional int32 p = 2 [deprecated = true]; // Deprecated since 9.8 optional int64 ps = 3 [deprecated = true]; // Deprecated since 9.8 repeated Rule rules = 4; optional Actives actives = 5; optional sonarqube.ws.commons.Paging paging = 8; // Added in 9.8 } //WS api/rules/show message ShowResponse { optional Rule rule = 1; } message Rule { optional string key = 1; optional string repo = 2; optional string name = 3; optional string htmlDesc = 5 [deprecated=true]; optional string htmlNote = 6; optional string severity = 10 [deprecated=true]; optional string templateKey = 14; optional string lang = 19; optional sonarqube.ws.commons.RuleType type = 37 [deprecated=true]; optional DescriptionSections descriptionSections = 49; optional EducationPrinciples educationPrinciples = 50; optional sonarqube.ws.commons.CleanCodeAttribute cleanCodeAttribute = 52; optional Impacts impacts = 54; message Impacts { repeated sonarqube.ws.commons.Impact impacts = 1; } message DescriptionSections { repeated DescriptionSection descriptionSections = 1; } message DescriptionSection { required string key = 1; required string content = 2; optional Context context = 3; message Context { required string displayName = 1; required string key = 2; } } message EducationPrinciples { repeated string educationPrinciples = 1; } } message Actives { map actives = 1; } message ActiveList { repeated Active activeList = 1; } message Active { optional string qProfile = 1; optional string severity = 3; repeated Param params = 5; optional Impacts impacts = 9; message Param { optional string key = 1; optional string value = 2; } message Impacts { repeated sonarqube.ws.commons.Impact impacts = 1; } } ================================================ FILE: backend/server-api/src/main/proto/sonarqube/ws-settings.proto ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto3"; package sonarqube.ws.settings; option java_package = "org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws"; option java_outer_classname = "Settings"; option optimize_for = SPEED; // Response of GET api/settings/values message ValuesWsResponse { repeated Setting settings = 1; } message Setting { string key = 1; oneof valueOneOf { string value = 2; Values values = 3; FieldValues fieldValues = 4; } } message Values { repeated string values = 1; } message FieldValues { repeated Value fieldValues = 1; message Value { map value = 1; } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/MockWebServerExtensionWithProtobuf.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import com.google.protobuf.Message; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Iterator; import javax.annotation.Nullable; import mockwebserver3.MockResponse; import okio.Buffer; import org.sonarsource.sonarlint.core.commons.testutils.MockWebServerExtension; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import static org.junit.jupiter.api.Assertions.fail; public class MockWebServerExtensionWithProtobuf extends MockWebServerExtension { public void addProtobufResponse(String path, Message m) { try (var b = new Buffer()) { m.writeTo(b.outputStream()); responsesByPath.put(path, new MockResponse.Builder().body(b).build()); } catch (IOException e) { fail(e); } } public void addProtobufResponseDelimited(String path, Message... m) { try (var b = new Buffer()) { writeMessages(b.outputStream(), Arrays.asList(m).iterator()); responsesByPath.put(path, new MockResponse.Builder().body(b).build()); } } public static void writeMessages(OutputStream output, Iterator messages) { while (messages.hasNext()) { writeMessage(output, messages.next()); } } public static void writeMessage(OutputStream output, T message) { try { message.writeDelimitedTo(output); } catch (IOException e) { throw new IllegalStateException("failed to write message: " + message, e); } } public ServerApiHelper serverApiHelper() { return serverApiHelper(null); } public ServerApiHelper serverApiHelper(@Nullable String organizationKey) { return new ServerApiHelper(endpointParams(organizationKey), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); } public EndpointParams endpointParams() { return endpointParams(null); } public EndpointParams endpointParams(@Nullable String organizationKey) { return new EndpointParams(url("/"), url("/"), organizationKey != null, organizationKey); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/ServerApiHelperTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi; import java.net.HttpURLConnection; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.serverapi.exception.ForbiddenException; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.exception.TooManyRequestsException; import org.sonarsource.sonarlint.core.serverapi.exception.UnauthorizedException; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedServerResponseException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ServerApiHelperTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void concat_should_handle_base_url_with_trailing_slash() { var result = ServerApiHelper.concat("http://localhost:9000/", "/api/test"); assertThat(result).isEqualTo("http://localhost:9000/api/test"); } @Test void concat_should_handle_base_url_without_trailing_slash() { var result = ServerApiHelper.concat("http://localhost:9000", "/api/test"); assertThat(result).isEqualTo("http://localhost:9000/api/test"); } @Test void concat_should_handle_relative_path_without_leading_slash() { var result = ServerApiHelper.concat("http://localhost:9000", "api/test"); assertThat(result).isEqualTo("http://localhost:9000/api/test"); } @Test void concat_should_handle_empty_relative_path() { var result = ServerApiHelper.concat("http://localhost:9000", ""); assertThat(result).isEqualTo("http://localhost:9000/"); } @Test void handleError_should_throw_unauthorized_exception() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(UnauthorizedException.class) .hasMessage("Not authorized. Please check server credentials."); } @Test void handleError_should_throw_forbidden_exception() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(response.bodyAsString()).thenReturn("{\"errors\":[{\"msg\":\"Access denied\"}]}"); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(ForbiddenException.class) .hasMessage("Access denied"); } @Test void handleError_should_throw_forbidden_exception_with_default_message() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(response.bodyAsString()).thenReturn("{}"); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(ForbiddenException.class) .hasMessage("Access denied"); } @Test void handleError_should_throw_not_found_exception() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); when(response.url()).thenReturn("http://localhost:9000/api/test"); var error = ServerApiHelper.handleError(response); assertThat(error).isInstanceOf(NotFoundException.class); } @Test void handleError_should_throw_server_error_exception() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); when(response.url()).thenReturn("http://localhost:9000/api/test"); var error = ServerApiHelper.handleError(response); assertThat(error).isInstanceOf(ServerErrorException.class); } @Test void handleError_should_throw_too_many_requests_exception() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(ServerApiHelper.HTTP_TOO_MANY_REQUESTS); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(TooManyRequestsException.class) .hasMessage("Too many requests have been made."); } @Test void handleError_should_throw_illegal_state_exception_for_other_codes() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); when(response.url()).thenReturn("http://localhost:9000/api/test"); when(response.bodyAsString()).thenReturn("{\"errors\":[{\"msg\":\"Bad request\"}]}"); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(UnexpectedServerResponseException.class) .hasMessageContaining("Error 400 on http://localhost:9000/api/test: Bad request"); } @Test void handleError_should_throw_unexpected_response_body_exception_when_error_body_unexpected() { var response = mock(HttpClient.Response.class); when(response.code()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); when(response.url()).thenReturn("http://localhost:9000/api/test"); when(response.bodyAsString()).thenReturn("not json"); var error = ServerApiHelper.handleError(response); assertThat(error) .isInstanceOf(UnexpectedServerResponseException.class) .hasMessageContaining("Error 400 on http://localhost:9000/api/test"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/authentication/AuthenticationApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.authentication; import mockwebserver3.MockResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class AuthenticationApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private AuthenticationApi underTest; @BeforeEach void setUp() { underTest = new AuthenticationApi(mockServer.serverApiHelper()); } @Test void test_authentication_ok() { mockServer.addStringResponse("/api/authentication/validate?format=json", "{\"valid\": true}"); var validationResult = underTest.validate(new SonarLintCancelMonitor()); assertThat(validationResult.success()).isTrue(); assertThat(validationResult.message()).isEqualTo("Authentication successful"); } @Test void test_authentication_ko() { mockServer.addStringResponse("/api/authentication/validate?format=json", "{\"valid\": false}"); var validationResult = underTest.validate(new SonarLintCancelMonitor()); assertThat(validationResult.success()).isFalse(); assertThat(validationResult.message()).isEqualTo("Authentication failed"); } @Test void test_connection_issue() { mockServer.addResponse("/api/authentication/validate?format=json", new MockResponse.Builder().code(500).body("Foo").build()); var throwable = catchThrowable(() -> underTest.validate(new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(ServerErrorException.class); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/branches/ProjectBranchesApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.branches; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.BranchType; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.ProjectBranches; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class ProjectBranchesApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private final static String PROJECT_KEY = "project1"; private ProjectBranchesApi underTest; @BeforeEach void setUp() { underTest = new ProjectBranchesApi(mockServer.serverApiHelper()); } @Test void shouldDownloadBranches() { mockServer.addProtobufResponse("/api/project_branches/list.protobuf?project=" + PROJECT_KEY, ProjectBranches.ListWsResponse.newBuilder() .addBranches(ProjectBranches.Branch.newBuilder().setName("feature/foo").setIsMain(false).setType(BranchType.BRANCH)) .addBranches(ProjectBranches.Branch.newBuilder().setName("master").setIsMain(true).setType(BranchType.BRANCH)).build()); var branches = underTest.getAllBranches(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(branches).extracting(ServerBranch::getName, ServerBranch::isMain).containsExactlyInAnyOrder(tuple("master", true), tuple("feature/foo", false)); } @Test void shouldSkipShortLivingBranches() { var branchListResponseBuilder = ProjectBranches.ListWsResponse.newBuilder(); branchListResponseBuilder.addBranches(ProjectBranches.Branch.newBuilder().setName("branch-1.x").setIsMain(false).setType(BranchType.BRANCH)); branchListResponseBuilder.addBranches(ProjectBranches.Branch.newBuilder().setName("master").setIsMain(true).setType(BranchType.BRANCH)); branchListResponseBuilder.addBranches(ProjectBranches.Branch.newBuilder().setName("feature/my-long-branch").setIsMain(false).setType(BranchType.LONG)); branchListResponseBuilder.addBranches(ProjectBranches.Branch.newBuilder().setName("feature/my-short-branch").setIsMain(false).setType(BranchType.SHORT)); mockServer.addProtobufResponse("/api/project_branches/list.protobuf?project=" + PROJECT_KEY, branchListResponseBuilder.build()); var branches = underTest.getAllBranches(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(branches).extracting(ServerBranch::getName, ServerBranch::isMain) .containsExactlyInAnyOrder(tuple("master", true), tuple("branch-1.x", false), tuple("feature/my-long-branch", false)); } @Test void shouldReturnEmptyListOnMalformedResponse() { mockServer.addStringResponse("/api/project_branches/list.protobuf?project=project1", """ { "branches": [ { }\ ] }"""); var branches = underTest.getAllBranches(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(branches).isEmpty(); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/branches/ServerBranchTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.branches; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; class ServerBranchTests { @Test void serverBranchTest() { ServerBranch branch = new ServerBranch("foo", true); assertThat(branch.getName()).isEqualTo("foo"); assertThat(branch.isMain()).isTrue(); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/component/ComponentApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Components; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class ComponentApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private static final String PROJECT_KEY = "project1"; private ComponentApi underTest; @BeforeEach void setUp() { underTest = new ComponentApi(mockServer.serverApiHelper()); } @Test void should_return_empty_when_no_components_returned() { mockServer.addStringResponse("/api/components/search_projects?projectIds=project%3Akey", "{\"components\":[]}"); var result = underTest.searchProjects("project:key", new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_return_empty_when_response_is_invalid_json() { mockServer.addStringResponse("/api/components/search_projects?projectIds=project%3Akey", "invalid json"); var result = underTest.searchProjects("project:key", new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_get_project_key_by_project_id() { var projectId = "project:key"; var encodedProjectId = "project%3Akey"; var organization = "my-org"; underTest = new ComponentApi(mockServer.serverApiHelper(organization)); mockServer.addStringResponse("/api/components/search_projects?projectIds=" + encodedProjectId + "&organization=" + organization, "{\"components\":[{\"key\":\"projectKey\",\"name\":\"projectName\"}]}\n"); var result = underTest.searchProjects(projectId, new SonarLintCancelMonitor()); assertThat(result.projectKey()).isEqualTo("projectKey"); assertThat(result.projectName()).isEqualTo("projectName"); } @Test void should_return_empty_if_project_not_found() { var result = underTest.searchProjects("project:key", new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_get_files() { mockServer.addResponseFromResource("/api/components/tree.protobuf?qualifiers=FIL,UTS&component=project1&ps=500&p=1", "/update/component_tree.pb"); var files = underTest.getAllFileKeys(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(files).hasSize(187); assertThat(files.get(0)).isEqualTo("org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java"); } @Test void should_get_files_with_organization() { underTest = new ComponentApi(mockServer.serverApiHelper("myorg")); mockServer.addResponseFromResource("/api/components/tree.protobuf?qualifiers=FIL,UTS&component=project1&organization=myorg&ps=500&p=1", "/update/component_tree.pb"); var files = underTest.getAllFileKeys(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(files).hasSize(187); assertThat(files.get(0)).isEqualTo("org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java"); } @Test void should_get_empty_files_if_tree_is_empty() { mockServer.addResponseFromResource("/api/components/tree.protobuf?qualifiers=FIL,UTS&component=project1&ps=500&p=1", "/update/empty_component_tree.pb"); var files = underTest.getAllFileKeys(PROJECT_KEY, new SonarLintCancelMonitor()); assertThat(files).isEmpty(); } @Test void should_get_all_projects() { mockServer.addProtobufResponse("/api/components/search.protobuf?qualifiers=TRK&ps=500&p=1", Components.SearchWsResponse.newBuilder() .addComponents(Components.Component.newBuilder().setKey("projectKey").setName("projectName").build()).build()); mockServer.addProtobufResponse("/api/components/search.protobuf?qualifiers=TRK&ps=500&p=2", Components.SearchWsResponse.newBuilder().build()); var projects = underTest.getAllProjects(new SonarLintCancelMonitor()); assertThat(projects) .extracting("key", "name") .containsOnly(tuple("projectKey", "projectName")); } @Test void should_get_all_projects_with_organization() { mockServer.addProtobufResponse("/api/components/search.protobuf?qualifiers=TRK&organization=org%3Akey&ps=500&p=1", Components.SearchWsResponse.newBuilder() .addComponents(Components.Component.newBuilder().setKey("projectKey").setName("projectName").build()).build()); mockServer.addProtobufResponse("/api/components/search.protobuf?qualifiers=TRK&organization=org%3Akey&ps=500&p=2", Components.SearchWsResponse.newBuilder().build()); var componentApi = new ComponentApi(mockServer.serverApiHelper("org:key")); var projects = componentApi.getAllProjects(new SonarLintCancelMonitor()); assertThat(projects) .extracting("key", "name") .containsOnly(tuple("projectKey", "projectName")); } @Test void should_get_project_details() { mockServer.addProtobufResponse("/api/components/show.protobuf?component=project%3Akey", Components.ShowWsResponse.newBuilder() .setComponent(Components.Component.newBuilder().setKey("projectKey").setName("projectName").build()).build()); var project = underTest.getProject("project:key", new SonarLintCancelMonitor()); assertThat(project).hasValueSatisfying(p -> { assertThat(p.key()).isEqualTo("projectKey"); assertThat(p.name()).isEqualTo("projectName"); }); } @Test void should_get_empty_project_details_if_request_fails() { var project = underTest.getProject("project:key", new SonarLintCancelMonitor()); assertThat(project).isEmpty(); } @Test void should_get_ancestor_key() { mockServer.addProtobufResponse("/api/components/show.protobuf?component=project%3Akey", Components.ShowWsResponse.newBuilder() .addAncestors(Components.Component.newBuilder().setKey("ancestorKey").build()).build()); var project = underTest.fetchFirstAncestorKey("project:key", new SonarLintCancelMonitor()); assertThat(project).contains("ancestorKey"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/component/ServerProjectTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.component; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ServerProjectTests { @Test void testGetters() { ServerProject project = new ServerProject("key", "name", false); assertThat(project.key()).isEqualTo("key"); assertThat(project.name()).isEqualTo("name"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/developers/DevelopersApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.developers; import java.time.ZonedDateTime; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.NotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.tuple; class DevelopersApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private DevelopersApi underTest; @BeforeEach void setUp() { underTest = new DevelopersApi(mockServer.serverApiHelper()); } @Test void should_return_events_for_a_given_project_key() { mockServer.addStringResponse("/api/developers/search_events?projects=projectKey&from=2022-01-01T12%3A00%3A00%2B0000", "{\"events\": [" + "{" + "\"category\": \"cat\"," + "\"message\": \"msg\"," + "\"link\": \"lnk\"," + "\"project\": \"projectKey\"," + "\"date\": \"2022-01-01T08:00:00+0000\"" + "}" + "]" + "}"); var response = underTest.searchEvents(Map.of("projectKey", ZonedDateTime.parse("2022-01-01T12:00:00Z")), new SonarLintCancelMonitor()); assertThat(response.events()) .extracting("category", "message", "link", "project", "date") .containsOnly(tuple("cat", "msg", "lnk", "projectKey", ZonedDateTime.parse("2022-01-01T08:00:00Z"))); } @Test void should_throw_if_a_field_is_missing_in_one_of_them() { mockServer.addStringResponse("/api/developers/search_events?projects=projectKey&from=2022-01-01T12%3A00%3A00%2B0000", "{\"events\": [" + "{" + "\"message\": \"msg\"," + "\"link\": \"lnk\"," + "\"project\": \"projectKey\"," + "\"date\": \"2022-01-01T08:00:00+0000\"" + "}" + "]" + "}"); var throwable = catchThrowable(() -> underTest.searchEvents(Map.of("projectKey", ZonedDateTime.parse("2022-01-01T12:00:00Z")), new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } @Test void should_throw_if_the_request_fails() { var throwable = catchThrowable(() -> underTest.searchEvents(Map.of("projectKey", ZonedDateTime.parse("2022-01-01T12:00:00Z")), new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(NotFoundException.class); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/exception/ProjectNotFoundExceptionTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.exception; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ProjectNotFoundExceptionTests { @Test void show_organization_key() { var ex = new ProjectNotFoundException("module", "organization"); assertThat(ex.getMessage()).isEqualTo("Project with key 'module' in organization 'organization' not found on SonarQube Cloud (was it deleted?)"); } @Test void organization_key_missing() { var ex = new ProjectNotFoundException("module", null); assertThat(ex.getMessage()).isEqualTo("Project with key 'module' not found on your SonarQube Server instance (was it deleted?)"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/fixsuggestions/FixSuggestionsApiTest.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.fixsuggestions; import java.util.List; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class FixSuggestionsApiTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private FixSuggestionsApi underTest; @BeforeEach void setUp() { underTest = new FixSuggestionsApi(mockServer.serverApiHelper()); } @Nested class GetAiSuggestion { @Test void it_should_throw_an_exception_if_the_body_is_malformed() { mockServer.addStringResponse("/api/v2/fix-suggestions/ai-suggestions", """ { "id": "XXX } """); var throwable = catchThrowable(() -> underTest.getAiSuggestion( new AiSuggestionRequestBodyDto("orgKey", "projectKey", new AiSuggestionRequestBodyDto.Issue("message", 0, 0, "rule:key", "source")), new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } @Test void it_should_return_the_generated_suggestion_for_sonarqube_cloud() { mockServer.addStringResponse("/fix-suggestions/ai-suggestions", """ { "id": "9d4e18f6-f79f-41ad-a480-1c96bd58d58f", "explanation": "This is the way", "changes": [ { "startLine": 0, "endLine": 0, "newCode": "This is the new code" } ] } """); underTest = new FixSuggestionsApi(mockServer.serverApiHelper("orgKey")); var response = underTest.getAiSuggestion(new AiSuggestionRequestBodyDto("orgKey", "projectKey", new AiSuggestionRequestBodyDto.Issue("message", 0, 0, "rule:key", "source")), new SonarLintCancelMonitor()); assertThat(response) .isEqualTo(new AiSuggestionResponseBodyDto(UUID.fromString("9d4e18f6-f79f-41ad-a480-1c96bd58d58f"), "This is the way", List.of(new AiSuggestionResponseBodyDto.ChangeDto(0, 0, "This is the new code")))); } @Test void it_should_return_the_generated_suggestion_for_sonarqube_server() { mockServer.addStringResponse("/api/v2/fix-suggestions/ai-suggestions", """ { "id": "9d4e18f6-f79f-41ad-a480-1c96bd58d58f", "explanation": "This is the way", "changes": [ { "startLine": 0, "endLine": 0, "newCode": "This is the new code" } ] } """); var response = underTest.getAiSuggestion(new AiSuggestionRequestBodyDto("orgKey", "projectKey", new AiSuggestionRequestBodyDto.Issue("message", 0, 0, "rule:key", "source")), new SonarLintCancelMonitor()); assertThat(response) .isEqualTo(new AiSuggestionResponseBodyDto(UUID.fromString("9d4e18f6-f79f-41ad-a480-1c96bd58d58f"), "This is the way", List.of(new AiSuggestionResponseBodyDto.ChangeDto(0, 0, "This is the new code")))); } } @Nested class GetSupportedRules { @Test void it_should_throw_an_exception_if_the_body_is_malformed() { mockServer.addStringResponse("/api/v2/fix-suggestions/supported-rules", """ [ """); var throwable = catchThrowable(() -> underTest.getSupportedRules(new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } @Test void it_should_return_the_list_of_supported_rules_for_sonarqube_cloud() { mockServer.addStringResponse("/fix-suggestions/supported-rules", """ { "rules": ["repo:rule1", "repo:rule2"] } """); underTest = new FixSuggestionsApi(mockServer.serverApiHelper("orgKey")); var response = underTest.getSupportedRules(new SonarLintCancelMonitor()); assertThat(response) .isEqualTo(new SupportedRulesResponseDto(Set.of("repo:rule1", "repo:rule2"))); } @Test void it_should_return_the_list_of_supported_rules_for_sonarqube_server() { mockServer.addStringResponse("/api/v2/fix-suggestions/supported-rules", """ { "rules": ["repo:rule1", "repo:rule2"] } """); var response = underTest.getSupportedRules(new SonarLintCancelMonitor()); assertThat(response) .isEqualTo(new SupportedRulesResponseDto(Set.of("repo:rule1", "repo:rule2"))); } } @Nested class GetOrganizationConfigs { @Test void it_should_throw_an_exception_if_the_body_is_malformed() { mockServer.addStringResponse("/fix-suggestions/organization-configs/orgId", """ [ """); var throwable = catchThrowable(() -> underTest.getOrganizationConfigs("orgId", new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } @Test void it_should_return_the_organization_config() { mockServer.addStringResponse("/fix-suggestions/organization-configs/orgId", """ { "organizationId": "orgId", "enablement": "DISABLED", "organizationEligible": true, "aiCodeFix": { "enablement": "DISABLED", "organizationEligible": true } } """); var response = underTest.getOrganizationConfigs("orgId", new SonarLintCancelMonitor()); assertThat(response) .isEqualTo(new OrganizationConfigsResponseDto("orgId", new AiCodeFixConfiguration(SuggestionFeatureEnablement.DISABLED, null, true))); } } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/hotspot/HotspotApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.hotspot; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.UrlUtils; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Hotspots; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertThrows; class HotspotApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private HotspotApi underTest; @BeforeEach void setUp() { underTest = new ServerApi(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth()).hotspot(); } @Test void it_should_call_the_expected_api_endpoint_when_fetching_hotspot_details() { underTest.fetch("h", new SonarLintCancelMonitor()); var recordedRequest = mockServer.takeRequest(); assertThat(recordedRequest.getPath()).isEqualTo("/api/hotspots/show.protobuf?hotspot=h"); } @Test void it_should_urlencode_the_hotspot_and_project_keys_when_fetching_hotspot_details() { underTest.fetch("hot/spot", new SonarLintCancelMonitor()); var recordedRequest = mockServer.takeRequest(); assertThat(recordedRequest.getPath()).isEqualTo("/api/hotspots/show.protobuf?hotspot=hot%2Fspot"); } @Test void it_should_adapt_and_return_the_hotspot_details() { mockServer.addProtobufResponse("/api/hotspots/show.protobuf?hotspot=h", Hotspots.ShowWsResponse.newBuilder() .setMessage("message") .setComponent(Hotspots.Component.newBuilder().setPath("path").setKey("myproject:path")) .setTextRange(Common.TextRange.newBuilder().setStartLine(2).setStartOffset(7).setEndLine(4).setEndOffset(9).build()) .setAuthor("author") .setStatus("REVIEWED") .setResolution("SAFE") .setRule(Hotspots.Rule.newBuilder().setKey("key") .setName("name") .setSecurityCategory("category") .setVulnerabilityProbability("HIGH") .setRiskDescription("risk") .setVulnerabilityDescription("vulnerability") .setFixRecommendations("fix") .build()) .build()); mockServer.addStringResponse("/api/sources/raw?key=" + UrlUtils.urlEncode("myproject:path"), "Even\nBefore My\n\tCode\n Snippet And\n After"); var remoteHotspot = underTest.fetch("h", new SonarLintCancelMonitor()); assertThat(remoteHotspot).isNotEmpty(); var hotspot = remoteHotspot.get(); assertThat(hotspot.message).isEqualTo("message"); assertThat(hotspot.filePath).isEqualTo(Path.of("path")); assertThat(hotspot.textRange).usingRecursiveComparison().isEqualTo(new TextRangeWithHash(2, 7, 4, 9, "")); assertThat(hotspot.author).isEqualTo("author"); assertThat(hotspot.status).isEqualTo(ServerHotspotDetails.Status.REVIEWED); assertThat(hotspot.resolution).isEqualTo(ServerHotspotDetails.Resolution.SAFE); assertThat(hotspot.rule.key).isEqualTo("key"); assertThat(hotspot.rule.name).isEqualTo("name"); assertThat(hotspot.rule.securityCategory).isEqualTo("category"); assertThat(hotspot.rule.vulnerabilityProbability).isEqualTo(VulnerabilityProbability.HIGH); assertThat(hotspot.rule.riskDescription).isEqualTo("risk"); assertThat(hotspot.rule.vulnerabilityDescription).isEqualTo("vulnerability"); assertThat(hotspot.rule.fixRecommendations).isEqualTo("fix"); assertThat(hotspot.codeSnippet).isEqualTo("My\n\tCode\n Snippet"); } @Test void it_should_extract_single_line_snippet() { mockServer.addProtobufResponse("/api/hotspots/show.protobuf?hotspot=h", Hotspots.ShowWsResponse.newBuilder() .setMessage("message") .setComponent(Hotspots.Component.newBuilder().setPath("path").setKey("myproject:path")) .setTextRange(Common.TextRange.newBuilder().setStartLine(2).setStartOffset(7).setEndLine(2).setEndOffset(9).build()) .setAuthor("author") .setStatus("REVIEWED") .setResolution("SAFE") .setRule(Hotspots.Rule.newBuilder().setKey("key") .setName("name") .setSecurityCategory("category") .setVulnerabilityProbability("HIGH") .setRiskDescription("risk") .setVulnerabilityDescription("vulnerability") .setFixRecommendations("fix") .build()) .build()); mockServer.addStringResponse("/api/sources/raw?key=" + UrlUtils.urlEncode("myproject:path"), "Even\nBefore My\n\tCode\n Snippet And\n After"); var remoteHotspot = underTest.fetch("h", new SonarLintCancelMonitor()); assertThat(remoteHotspot).isNotEmpty(); var hotspot = remoteHotspot.get(); assertThat(hotspot.codeSnippet).isEqualTo("My"); } @Test void it_should_return_empty_optional_when_ws_client_throws_an_exception() { var remoteHotspot = underTest.fetch("h", new SonarLintCancelMonitor()); assertThat(remoteHotspot).isEmpty(); } @Test void it_should_throw_when_parser_throws_an_exception() { mockServer.addProtobufResponse("/api/hotspots/show.protobuf?hotspot=h", Issues.SearchWsResponse.newBuilder().build()); var cancelMonitor = new SonarLintCancelMonitor(); assertThrows(IllegalArgumentException.class, () -> underTest.fetch("h", cancelMonitor)); } @Test void it_should_return_no_resolution_status_when_not_available() { mockServer.addProtobufResponse("/api/hotspots/show.protobuf?hotspot=h", Hotspots.ShowWsResponse.newBuilder() .setComponent(Hotspots.Component.newBuilder().setPath("path")) .setTextRange(Common.TextRange.newBuilder().setStartLine(1).setStartOffset(2).setEndLine(3).setEndOffset(4).build()) .setStatus("TO_REVIEW") .setRule(Hotspots.Rule.newBuilder().setKey("key") .setName("name") .setSecurityCategory("category") .setVulnerabilityProbability("HIGH") .setRiskDescription("risk") .setVulnerabilityDescription("vulnerability") .setFixRecommendations("fix") .build()) .build()); var remoteHotspot = underTest.fetch("h", new SonarLintCancelMonitor()); assertThat(remoteHotspot).isNotEmpty(); var hotspot = remoteHotspot.get(); assertThat(hotspot.resolution).isNull(); } @Test void it_should_map_acknowledged_status_for_show() { mockServer.addProtobufResponse("/api/hotspots/show.protobuf?hotspot=h", Hotspots.ShowWsResponse.newBuilder() .setComponent(Hotspots.Component.newBuilder().setPath("path")) .setTextRange(Common.TextRange.newBuilder().setStartLine(1).setStartOffset(2).setEndLine(3).setEndOffset(4).build()) .setStatus("REVIEWED") .setResolution("ACKNOWLEDGED") .setRule(Hotspots.Rule.newBuilder().setKey("key") .setName("name") .setSecurityCategory("category") .setVulnerabilityProbability("HIGH") .setRiskDescription("risk") .setVulnerabilityDescription("vulnerability") .setFixRecommendations("fix") .build()) .build()); var remoteHotspot = underTest.fetch("h", new SonarLintCancelMonitor()); assertThat(remoteHotspot).isNotEmpty(); var hotspot = remoteHotspot.get(); assertThat(hotspot.resolution).isEqualTo(ServerHotspotDetails.Resolution.ACKNOWLEDGED); } @Test void it_should_fetch_project_hotspots() { mockServer.addProtobufResponse("/api/hotspots/search.protobuf?projectKey=p&branch=branch&ps=500&p=1", Hotspots.SearchWsResponse.newBuilder() .setPaging(Common.Paging.newBuilder().setTotal(1).build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path1") .setTextRange(Common.TextRange.newBuilder().setStartLine(1).setStartOffset(2).setEndLine(3).setEndOffset(4).build()) .setStatus("TO_REVIEW") .setKey("hotspotKey1") .setCreationDate("2020-09-21T12:46:39+0000") .setRuleKey("ruleKey1") .setMessage("message1") .setVulnerabilityProbability("HIGH") .build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path2") .setTextRange(Common.TextRange.newBuilder().setStartLine(5).setStartOffset(6).setEndLine(7).setEndOffset(8).build()) .setStatus("REVIEWED") .setResolution("SAFE") .setKey("hotspotKey2") .setCreationDate("2020-09-22T12:46:39+0000") .setRuleKey("ruleKey2") .setMessage("message2") .setVulnerabilityProbability("LOW") .build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path3") .setTextRange(Common.TextRange.newBuilder().setStartLine(9).setStartOffset(10).setEndLine(11).setEndOffset(12).build()) .setStatus("REVIEWED") .setResolution("ACKNOWLEDGED") .setKey("hotspotKey3") .setCreationDate("2020-09-23T12:46:39+0000") .setRuleKey("ruleKey3") .setMessage("message3") .setVulnerabilityProbability("LOW") .build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path4") .setTextRange(Common.TextRange.newBuilder().setStartLine(13).setStartOffset(14).setEndLine(15).setEndOffset(16).build()) .setStatus("REVIEWED") .setResolution("FIXED") .setKey("hotspotKey4") .setCreationDate("2020-09-24T12:46:39+0000") .setRuleKey("ruleKey4") .setMessage("message4") .setVulnerabilityProbability("MEDIUM") .build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path5") .setTextRange(Common.TextRange.newBuilder().setStartLine(17).setStartOffset(18).setEndLine(19).setEndOffset(20).build()) .setStatus("REVIEWED") .setKey("hotspotKey5") .setCreationDate("2020-09-25T12:46:39+0000") .setRuleKey("ruleKey5") .setMessage("message5") .setVulnerabilityProbability("LOW") .build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path6") .setTextRange(Common.TextRange.newBuilder().setStartLine(21).setStartOffset(22).setEndLine(23).setEndOffset(24).build()) .setStatus("REVIEWED") .setResolution("UNKNOWN") .setKey("hotspotKey6") .setCreationDate("2020-09-25T12:46:39+0000") .setRuleKey("ruleKey6") .setMessage("message6") .setVulnerabilityProbability("LOW") .build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path1").setPath("path1").build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path2").setPath("path2").build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path3").setPath("path3").build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path4").setPath("path4").build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path5").setPath("path5").build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path6").setPath("path6").build()) .build()); var hotspots = underTest.getAll("p", "branch", new SonarLintCancelMonitor()); assertThat(hotspots) .extracting("key", "ruleKey", "message", "filePath", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "creationDate", "status") .containsExactly( tuple("hotspotKey1", "ruleKey1", "message1", Path.of("path1"), 1, 2, 3, 4, LocalDateTime.of(2020, 9, 21, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.TO_REVIEW), tuple("hotspotKey2", "ruleKey2", "message2", Path.of("path2"), 5, 6, 7, 8, LocalDateTime.of(2020, 9, 22, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.SAFE), tuple("hotspotKey3", "ruleKey3", "message3", Path.of("path3"), 9, 10, 11, 12, LocalDateTime.of(2020, 9, 23, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.ACKNOWLEDGED), tuple("hotspotKey4", "ruleKey4", "message4", Path.of("path4"), 13, 14, 15, 16, LocalDateTime.of(2020, 9, 24, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.FIXED), tuple("hotspotKey5", "ruleKey5", "message5", Path.of("path5"), 17, 18, 19, 20, LocalDateTime.of(2020, 9, 25, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.SAFE), tuple("hotspotKey6", "ruleKey6", "message6", Path.of("path6"), 21, 22, 23, 24, LocalDateTime.of(2020, 9, 25, 12, 46, 39).toInstant(ZoneOffset.UTC), HotspotReviewStatus.TO_REVIEW)); } @Test void it_should_fetch_file_hotspots() { mockServer.addProtobufResponse("/api/hotspots/search.protobuf?projectKey=p&files=path%2Fto%2Ffile.ext&branch=branch&ps=500&p=1", Hotspots.SearchWsResponse.newBuilder() .setPaging(Common.Paging.newBuilder().setTotal(1).build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path/to/file.ext") .setTextRange(Common.TextRange.newBuilder().setStartLine(1).setStartOffset(2).setEndLine(3).setEndOffset(4).build()) .setStatus("TO_REVIEW") .setKey("hotspotKey1") .setCreationDate("2020-09-21T12:46:39+0000") .setRuleKey("ruleKey1") .setMessage("message1") .setVulnerabilityProbability("HIGH") .build()) .addComponents(Hotspots.Component.newBuilder().setKey("component:path/to/file.ext").setPath("path/to/file.ext").build()) .build()); var hotspots = underTest.getFromFile("p", Path.of("path/to/file.ext"), "branch", new SonarLintCancelMonitor()); assertThat(hotspots) .extracting("key", "ruleKey", "message", "filePath", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "creationDate", "status") .containsExactly( tuple("hotspotKey1", "ruleKey1", "message1", Path.of("path/to/file.ext"), 1, 2, 3, 4, ZonedDateTime.of(2020, 9, 21, 12, 46, 39, 0, ZoneId.of("UTC")).toInstant(), HotspotReviewStatus.TO_REVIEW)); } @Test void it_should_log_when_hotspot_component_is_missing() { mockServer.addProtobufResponse("/api/hotspots/search.protobuf?projectKey=p&branch=branch&ps=500&p=1", Hotspots.SearchWsResponse.newBuilder() .setPaging(Common.Paging.newBuilder().setTotal(1).build()) .addHotspots(Hotspots.SearchWsResponse.Hotspot.newBuilder() .setComponent("component:path") .build()) .build()); underTest.getAll("p", "branch", new SonarLintCancelMonitor()); assertThat(logTester.logs()) .contains("Error while fetching security hotspots, the component 'component:path' is missing"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/hotspot/ServerHotspotDetailsTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.hotspot; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRange; import static org.assertj.core.api.Assertions.assertThat; class ServerHotspotDetailsTests { @Test void it_should_populate_fields_with_constructor_parameters() { var hotspot = new ServerHotspotDetails("message", Path.of("path"), new TextRange(0, 1, 2, 3), "author", ServerHotspotDetails.Status.TO_REVIEW, ServerHotspotDetails.Resolution.FIXED, new ServerHotspotDetails.Rule( "key", "name", "category", VulnerabilityProbability.HIGH, "risk", "vulnerability", "fix"), "some code \n content", true); assertThat(hotspot.message).isEqualTo("message"); assertThat(hotspot.filePath).isEqualTo(Path.of("path")); assertThat(hotspot.textRange.getStartLine()).isZero(); assertThat(hotspot.textRange.getStartLineOffset()).isEqualTo(1); assertThat(hotspot.textRange.getEndLine()).isEqualTo(2); assertThat(hotspot.textRange.getEndLineOffset()).isEqualTo(3); assertThat(hotspot.author).isEqualTo("author"); assertThat(hotspot.status).isEqualTo(ServerHotspotDetails.Status.TO_REVIEW); assertThat(hotspot.resolution).isEqualTo(ServerHotspotDetails.Resolution.FIXED); assertThat(hotspot.rule.key).isEqualTo("key"); assertThat(hotspot.rule.name).isEqualTo("name"); assertThat(hotspot.rule.securityCategory).isEqualTo("category"); assertThat(hotspot.rule.vulnerabilityProbability).isEqualTo(VulnerabilityProbability.HIGH); assertThat(hotspot.rule.riskDescription).isEqualTo("risk"); assertThat(hotspot.rule.vulnerabilityDescription).isEqualTo("vulnerability"); assertThat(hotspot.rule.fixRecommendations).isEqualTo("fix"); assertThat(hotspot.codeSnippet).isEqualTo("some code \n content"); assertThat(hotspot.canChangeStatus).isTrue(); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/issue/IssueApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.issue; import java.nio.file.Path; import java.util.Set; import mockwebserver3.MockResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.scanner.protocol.input.ScannerInput; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.entry; import static org.sonarsource.sonarlint.core.serverapi.UrlUtils.urlEncode; class IssueApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private IssueApi underTest; @BeforeEach void setUp() { underTest = new IssueApi(mockServer.serverApiHelper()); } @Test void should_download_all_issues_as_batch() { mockServer.addProtobufResponseDelimited("/batch/issues?key=keyyy", ScannerInput.ServerIssue.newBuilder().setRuleKey("ruleKey").build()); var issues = underTest.downloadAllFromBatchIssues("keyyy", null, new SonarLintCancelMonitor()); assertThat(issues) .extracting("ruleKey") .containsOnly("ruleKey"); } @Test void should_download_all_issues_as_batch_from_branch() { mockServer.addProtobufResponseDelimited("/batch/issues?key=keyyy&branch=branchName", ScannerInput.ServerIssue.newBuilder().setRuleKey("ruleKey").build()); var issues = underTest.downloadAllFromBatchIssues("keyyy", "branchName", new SonarLintCancelMonitor()); assertThat(issues) .extracting("ruleKey") .containsOnly("ruleKey"); } @Test void should_return_no_batch_issue_if_download_is_forbidden() { mockServer.addResponse("/batch/issues?key=keyyy", new MockResponse.Builder().code(403).build()); var issues = underTest.downloadAllFromBatchIssues("keyyy", null, new SonarLintCancelMonitor()); assertThat(issues).isEmpty(); } @Test void should_return_no_batch_issue_if_endpoint_is_not_found() { mockServer.addResponse("/batch/issues?key=keyyy", new MockResponse.Builder().code(404).build()); var issues = underTest.downloadAllFromBatchIssues("keyyy", null, new SonarLintCancelMonitor()); assertThat(issues).isEmpty(); } @Test void should_throw_an_error_if_batch_issue_download_fails() { mockServer.addResponse("/batch/issues?key=keyyy", new MockResponse.Builder().code(500).build()); var throwable = catchThrowable(() -> underTest.downloadAllFromBatchIssues("keyyy", null, new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(ServerErrorException.class); } @Test void should_throw_an_error_if_batch_issue_body__format_is_unexpected() { mockServer.addStringResponse("/batch/issues?key=keyyy", "nope"); var throwable = catchThrowable(() -> underTest.downloadAllFromBatchIssues("keyyy", null, new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(IllegalStateException.class); } @Test void should_download_all_vulnerabilities() { mockServer.addProtobufResponse("/api/issues/search.protobuf?statuses=OPEN,CONFIRMED,REOPENED,RESOLVED&types=VULNERABILITY&componentKeys=keyyy&components=keyyy&rules=ruleKey&ps=500&p=1", Issues.SearchWsResponse.newBuilder().addIssues(Issues.Issue.newBuilder().setKey("issueKey").build()).build()); mockServer.addProtobufResponse("/api/issues/search.protobuf?statuses=OPEN,CONFIRMED,REOPENED,RESOLVED&types=VULNERABILITY&componentKeys=keyyy&components=keyyy&rules=ruleKey&ps=500&p=2", Issues.SearchWsResponse.newBuilder().addComponents(Issues.Component.newBuilder().setKey("componentKey").setPath("componentPath").build()).build()); var result = underTest.downloadVulnerabilitiesForRules("keyyy", Set.of("ruleKey"), null, new SonarLintCancelMonitor()); assertThat(result.getIssues()) .extracting("key") .containsOnly("issueKey"); assertThat(result.getComponentPathsByKey()) .containsOnly(entry("componentKey", Path.of("componentPath"))); } @Test void should_fetch_server_issue_by_key() { var issueKey = "issueKey"; var path = "/home/file.java"; var projectKey = "projectKey"; mockServer.addProtobufResponse("/api/issues/search.protobuf?issues=".concat(urlEncode(issueKey)).concat("&componentKeys=").concat(projectKey).concat("&components=").concat(projectKey).concat("&ps=1&p=1"), Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder().setKey(issueKey).build()) .addComponents(Issues.Component.newBuilder().setPath(path).build()) .setRules(Issues.SearchWsResponse.newBuilder().getRulesBuilder().addRules(Common.Rule.newBuilder().setKey("ruleKey").build())) .build()); var serverIssueDetails = underTest.fetchServerIssue(issueKey, projectKey, "", "", new SonarLintCancelMonitor()); assertThat(serverIssueDetails).isPresent(); assertThat(serverIssueDetails.get().key).isEqualTo(issueKey); assertThat(serverIssueDetails.get().path).isEqualTo(Path.of(path)); } @Test void should_not_fail_when_no_issue_found_by_key() { mockServer.addProtobufResponse("/api/issues/search.protobuf?issues=".concat(urlEncode("qwert")).concat("&componentKeys=myProject").concat("&components=myProject").concat("&ps=1&p=1"), Issues.SearchWsResponse.newBuilder().addIssues(Issues.Issue.newBuilder().build()).build()); var serverIssueDetails = underTest.fetchServerIssue("non-existent", "myProject", "", "", new SonarLintCancelMonitor()); assertThat(serverIssueDetails).isEmpty(); assertThat(logTester.logs()).contains("Error while fetching issue"); } @Test void should_not_fetch_server_issue_by_key_with_no_matching_component() { var issueKey = "issueKey"; var path = "/home/file.java"; mockServer.addProtobufResponse("/api/issues/search.protobuf?issues=".concat(urlEncode(issueKey)).concat("&componentKeys=differentIssueComponent").concat("&components=differentIssueComponent").concat("&ps=1&p=1"), Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder().setKey(issueKey).setComponent("issueComponent").build()) .addComponents(Issues.Component.newBuilder().setPath(path).setKey("differentIssueComponent").build()) .setRules(Issues.SearchWsResponse.newBuilder().getRulesBuilder().addRules(Common.Rule.newBuilder().setKey("ruleKey").build())) .build()); var serverIssueDetails = underTest.fetchServerIssue(issueKey, "differentIssueComponent", "", "", new SonarLintCancelMonitor()); assertThat(serverIssueDetails).isEmpty(); assertThat(logTester.logs()).contains("No path found in components for the issue with key 'issueKey'"); } @Test void should_fetch_branch_issue() { var issueKey = "issueKey"; var path = "/home/file.java"; var projectKey = "projectKey"; var branch = "branch"; mockServer.addProtobufResponse("/api/issues/search.protobuf?issues=".concat(urlEncode(issueKey)) .concat("&componentKeys=").concat(projectKey) .concat("&components=").concat(projectKey) .concat("&ps=1&p=1") .concat("&branch=").concat(branch), Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder().setKey(issueKey).build()) .addComponents(Issues.Component.newBuilder().setPath(path).build()) .setRules(Issues.SearchWsResponse.newBuilder().getRulesBuilder().addRules(Common.Rule.newBuilder().setKey("ruleKey").build())) .build()); var serverIssueDetails = underTest.fetchServerIssue(issueKey, projectKey, branch, "", new SonarLintCancelMonitor()); assertThat(serverIssueDetails).isPresent(); assertThat(serverIssueDetails.get().key).isEqualTo(issueKey); assertThat(serverIssueDetails.get().path).isEqualTo(Path.of(path)); } @Test void should_fetch_pull_request_issue() { var issueKey = "issueKey"; var path = "/home/file.java"; var projectKey = "projectKey"; var pullRequest = "1234"; mockServer.addProtobufResponse("/api/issues/search.protobuf?issues=".concat(urlEncode(issueKey)) .concat("&componentKeys=").concat(projectKey) .concat("&components=").concat(projectKey) .concat("&ps=1&p=1") .concat("&pullRequest=").concat(pullRequest), Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder().setKey(issueKey).build()) .addComponents(Issues.Component.newBuilder().setPath(path).build()) .setRules(Issues.SearchWsResponse.newBuilder().getRulesBuilder().addRules(Common.Rule.newBuilder().setKey("ruleKey").build())) .build()); var serverIssueDetails = underTest.fetchServerIssue(issueKey, projectKey, "prbranch", pullRequest, new SonarLintCancelMonitor()); assertThat(serverIssueDetails).isPresent(); assertThat(serverIssueDetails.get().key).isEqualTo(issueKey); assertThat(serverIssueDetails.get().path).isEqualTo(Path.of(path)); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/newcode/NewCodeApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.newcode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Measures; import org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.serverapi.newcode.NewCodeApi.getPeriodForServer; import static org.sonarsource.sonarlint.core.serverapi.newcode.NewCodeApi.getPeriodFromWs; class NewCodeApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String PROJECT = "project"; private static final String BRANCH = "branch"; private static final Version RECENT_SQ_VERSION = Version.create("10.2"); private static final Version SC_VERSION = Version.create("8.0.0.46314"); private static final String SOME_DATE = "2023-08-29T09:37:59+0000"; private static final long SOME_DATE_EPOCH_MILLIS = ServerApiUtils.parseOffsetDateTime(SOME_DATE).toInstant().toEpochMilli(); private ServerApiHelper mockApiHelper; private NewCodeApi underTest; @BeforeEach void setup() { mockApiHelper = mock(ServerApiHelper.class); underTest = new NewCodeApi(mockApiHelper); } @Test void getPeriodForNewSonarQube() { var response = Measures.ComponentWsResponse .newBuilder().setPeriod(Measures.Period.newBuilder() .setDate(SOME_DATE).build()) .build(); var period = getPeriodFromWs(response); assertThat(period.getDate()).isEqualTo(SOME_DATE); } @Test void getPeriodsForOldSonarQubeOrSonarCloud() { var response = Measures.ComponentWsResponse .newBuilder().setPeriods(Measures.Periods.newBuilder().addPeriods(Measures.Period.newBuilder() .setDate(SOME_DATE).build()).build()) .build(); var period = getPeriodFromWs(response); assertThat(period.getDate()).isEqualTo(SOME_DATE); } @Test void getPeriodFromServer() { var serverApiHelper = mock(ServerApiHelper.class); when(serverApiHelper.isSonarCloud()).thenReturn(true); var sonarCloud = getPeriodForServer(serverApiHelper, Version.create("9.2")); when(serverApiHelper.isSonarCloud()).thenReturn(false); var sonarQubeOld = getPeriodForServer(serverApiHelper, Version.create("8.0")); var sonarQubeNew = getPeriodForServer(serverApiHelper, Version.create("8.1")); assertThat(sonarCloud).isEqualTo("periods"); assertThat(sonarQubeOld).isEqualTo("periods"); assertThat(sonarQubeNew).isEqualTo("period"); } @Test void parseReferenceBranchPeriod() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("REFERENCE_BRANCH") .setParameter("referenceBranch") .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeReferenceBranch.class) .hasToString("Current new code definition (reference branch) is not supported"); assertThat(newCodeDefinition.isOnNewCode(0)).isTrue(); assertThat(newCodeDefinition.isSupported()).isFalse(); } @Test void parseNumberOfDaysPeriodFromSq() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("NUMBER_OF_DAYS") .setParameter("42") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeNumberOfDaysWithDate.class) .hasToString("From last 42 days"); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parseNumberOfDaysPeriodFromSc() { prepareScWsResponseWithPeriods(Measures.Period.newBuilder() .setMode("days") .setParameter("42") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, SC_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeNumberOfDaysWithDate.class) .hasToString("From last 42 days"); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parsePreviousVersionPeriodFromSq() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("PREVIOUS_VERSION") .setParameter("version") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodePreviousVersion.class) .hasToString("Since version version"); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parsePreviousVersionPeriodWithoutVersionFromSq() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("PREVIOUS_VERSION") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodePreviousVersion.class) .hasToString("Since " + NewCodeDefinition.formatEpochToDate(SOME_DATE_EPOCH_MILLIS)); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parsePreviousVersionPeriodFromSc() { prepareScWsResponseWithPeriods(Measures.Period.newBuilder() .setMode("previous_version") .setParameter("version") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, SC_VERSION, new SonarLintCancelMonitor()).orElseThrow(); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodePreviousVersion.class) .hasToString("Since version version"); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parseSpecificAnalysisPeriodFromSq() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("SPECIFIC_ANALYSIS") .setParameter("someAnalysisKey") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor()).orElseThrow(); var date = NewCodeDefinition.formatEpochToDate(SOME_DATE_EPOCH_MILLIS); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeSpecificAnalysis.class) .hasToString("Since analysis from " + date); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parseSpecificVersionPeriodFromSc() { prepareScWsResponseWithPeriods(Measures.Period.newBuilder() .setMode("version") .setParameter("X.Y.Z") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, SC_VERSION, new SonarLintCancelMonitor()).orElseThrow(); var date = NewCodeDefinition.formatEpochToDate(SOME_DATE_EPOCH_MILLIS); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeSpecificAnalysis.class) .hasToString("Since analysis from " + date); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parseSpecificDatePeriodFromSc() { prepareScWsResponseWithPeriods(Measures.Period.newBuilder() .setMode("date") .setDate(SOME_DATE) .build()); var newCodeDefinition = underTest.getNewCodeDefinition(PROJECT, BRANCH, SC_VERSION, new SonarLintCancelMonitor()).orElseThrow(); var date = NewCodeDefinition.formatEpochToDate(SOME_DATE_EPOCH_MILLIS); assertThat(newCodeDefinition).isInstanceOf(NewCodeDefinition.NewCodeSpecificAnalysis.class) .hasToString("Since analysis from " + date); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS + 1)).isTrue(); assertThat(newCodeDefinition.isOnNewCode(SOME_DATE_EPOCH_MILLIS - 1)).isFalse(); assertThat(newCodeDefinition.isSupported()).isTrue(); } @Test void parseUnknownModePeriod() { prepareSqWsResponseWithPeriod(Measures.Period.newBuilder() .setMode("Definitely not a supported mode") .setParameter("Whatever") .build()); assertThat(underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor())).isEmpty(); } @Test void failHttpCall() { when(mockApiHelper.get(anyString(), any(SonarLintCancelMonitor.class))) .thenThrow(new RuntimeException("Not good")); assertThat(underTest.getNewCodeDefinition(PROJECT, BRANCH, RECENT_SQ_VERSION, new SonarLintCancelMonitor())).isEmpty(); } void prepareSqWsResponseWithPeriod(Measures.Period period) { when(mockApiHelper.isSonarCloud()).thenReturn(false); var httpResponse = mock(HttpClient.Response.class); when(httpResponse.bodyAsStream()).thenReturn(Measures.ComponentWsResponse.newBuilder() .setPeriod(period) .build().toByteString().newInput()); when(mockApiHelper.get(eq("/api/measures/component.protobuf?additionalFields=period&metricKeys=projects&component=" + PROJECT + "&branch=" + BRANCH), any(SonarLintCancelMonitor.class))) .thenReturn(httpResponse); } void prepareScWsResponseWithPeriods(Measures.Period period) { when(mockApiHelper.isSonarCloud()).thenReturn(true); var httpResponse = mock(HttpClient.Response.class); when(httpResponse.bodyAsStream()).thenReturn(Measures.ComponentWsResponse.newBuilder() .setPeriods(Measures.Periods.newBuilder() .addPeriods(period) .build()) .build().toByteString().newInput()); when(mockApiHelper.get(eq("/api/measures/component.protobuf?additionalFields=periods&metricKeys=projects&component=" + PROJECT + "&branch=" + BRANCH), any(SonarLintCancelMonitor.class))) .thenReturn(httpResponse); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/organization/OrganizationApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.organization; import java.util.List; import java.util.UUID; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.Organization; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.SearchWsResponse; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Paging; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class OrganizationApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @Test void testListUserOrganizationWithMoreThan20Pages() { var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams("myOrg"), HttpClientProvider.forTesting().getHttpClientWithoutAuth())); for (var i = 0; i < 21; i++) { mockOrganizationsPage(i + 1, 10500); } var orgs = underTest.listUserOrganizations(new SonarLintCancelMonitor()); assertThat(orgs).hasSize(10500); } @Test void should_search_organization_details() { mockServer.addProtobufResponse("/api/organizations/search.protobuf?organizations=org%3Akey&ps=500&p=1", SearchWsResponse.newBuilder() .addOrganizations(Organization.newBuilder() .setKey("orgKey") .setName("orgName") .setDescription("orgDesc") .build()) .build()); mockServer.addProtobufResponse("/api/organizations/search.protobuf?organizations=org%3Akey&ps=500&p=2", SearchWsResponse.newBuilder().build()); var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth())); var organization = underTest.searchOrganization("org:key", new SonarLintCancelMonitor()); assertThat(organization).hasValueSatisfying(org -> { assertThat(org.getKey()).isEqualTo("orgKey"); assertThat(org.getName()).isEqualTo("orgName"); assertThat(org.getDescription()).isEqualTo("orgDesc"); }); } @Test void should_get_organization_by_key() { mockServer.addStringResponse("/organizations/organizations?organizationKey=org%3Akey&excludeEligibility=true", """ [{ "id": "orgId", "uuidV4": "f9cb252d-9f81-4e40-8b77-99fa13190b74" }] """); var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams("org:key"), HttpClientProvider.forTesting().getHttpClientWithoutAuth())); var organization = underTest.getOrganizationByKey(new SonarLintCancelMonitor()); assertThat(organization) .isEqualTo(new GetOrganizationsResponseDto("orgId", UUID.fromString("f9cb252d-9f81-4e40-8b77-99fa13190b74"))); } @Test void should_throw_if_get_organization_by_key_is_malformed() { mockServer.addStringResponse("/organizations/organizations?organizationKey=org%3Akey&excludeEligibility=true", """ [{ "id": "orgId", "uuidV4": "f9cb252d- """); var underTest = new OrganizationApi(new ServerApiHelper(mockServer.endpointParams("org:key"), HttpClientProvider.forTesting().getHttpClientWithoutAuth())); var throwable = catchThrowable(() -> underTest.getOrganizationByKey(new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } private void mockOrganizationsPage(int page, int total) { List orgs = IntStream.rangeClosed(1, 500) .mapToObj(i -> Organization.newBuilder().setKey("org_page" + page + "number" + i).build()) .toList(); var paging = Paging.newBuilder() .setPageSize(500) .setTotal(total) .setPageIndex(page) .build(); var response = Organizations.SearchWsResponse.newBuilder() .setPaging(paging) .addAllOrganizations(orgs) .build(); mockServer.addProtobufResponse("/api/organizations/search.protobuf?member=true&ps=500&p=" + page, response); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/organization/ServerOrganizationTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.organization; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverapi.proto.sonarcloud.ws.Organizations.Organization; import static org.assertj.core.api.Assertions.assertThat; class ServerOrganizationTests { @Test void testRoundTrip() { var org = Organization.newBuilder() .setName("name") .setKey("key") .setDescription("desc") .build(); ServerOrganization remoteOrg = new ServerOrganization(org); assertThat(remoteOrg.getKey()).isEqualTo("key"); assertThat(remoteOrg.getName()).isEqualTo("name"); assertThat(remoteOrg.getDescription()).isEqualTo("desc"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/plugins/PluginsApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.plugins; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class PluginsApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @Test void should_return_installed_plugins() { var underTest = new PluginsApi(mockServer.serverApiHelper()); mockServer.addStringResponse("/api/plugins/installed", "{\"plugins\": [" + "{\"key\": \"pluginKey\", \"hash\": \"de5308f43260d357acc97712ce4c5475\", \"filename\": \"plugin-1.0.0.1234.jar\", \"sonarLintSupported\": true}" + "]}"); var serverPlugins = underTest.getInstalled(new SonarLintCancelMonitor()); assertThat(serverPlugins) .extracting("key", "hash", "filename", "sonarLintSupported") .containsOnly(tuple("pluginKey", "de5308f43260d357acc97712ce4c5475", "plugin-1.0.0.1234.jar", true)); } @Test void should_return_plugin_content() { var underTest = new PluginsApi(mockServer.serverApiHelper()); mockServer.addStringResponse("/api/plugins/download?plugin=pluginKey", "content"); underTest.getPlugin("pluginKey", stream -> assertThat(stream).hasContent("content"), new SonarLintCancelMonitor()); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/projectbindings/ProjectBindingsApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.projectbindings; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import mockwebserver3.MockResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ProjectBindingsApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private ProjectBindingsApi underTest; @BeforeEach void setUp() { underTest = new ProjectBindingsApi(mockServer.serverApiHelper()); } @Nested class SonarQubeCloud { @Test void should_return_project_id_by_url() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/dop-translation/project-bindings?url=" + encodedUrl, "{\"bindings\":[{\"projectId\":\"proj:123\"}]}"); var result = underTest.getSQCProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isEqualTo(new SQCProjectBindingsResponse("proj:123")); } @Test void should_return_empty_when_no_bindings() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/dop-translation/project-bindings?url=" + encodedUrl, "{\"bindings\":[]}"); var result = underTest.getSQCProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_return_empty_when_invalid_json() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/dop-translation/project-bindings?url=" + encodedUrl, "this is not json"); var result = underTest.getSQCProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_return_empty_when_request_fails() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addResponse("/dop-translation/project-bindings?url=" + encodedUrl, new MockResponse.Builder().code(500).body("Internal error").build()); var result = underTest.getSQCProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isNull(); } } @Nested class SonarQubeServer { @Test void should_return_project_key_by_url() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/api/v2/dop-translation/project-bindings?repositoryUrl=" + encodedUrl, "{\"projectBindings\":[{\"projectId\":\"proj:123\",\"projectKey\":\"my-project-key\"}]}"); var result = underTest.getSQSProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isEqualTo(new SQSProjectBindingsResponse("proj:123", "my-project-key")); } @Test void should_return_empty_when_no_bindings() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/api/v2/dop-translation/project-bindings?repositoryUrl=" + encodedUrl, "{\"projectBindings\":[]}"); var result = underTest.getSQSProjectBindings(url, new SonarLintCancelMonitor()); assertThat(result).isNull(); } @Test void should_throw_when_invalid_json() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addStringResponse("/api/v2/dop-translation/project-bindings?repositoryUrl=" + encodedUrl, "this is not json"); var throwable = catchThrowable(() -> underTest.getSQSProjectBindings(url, new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } @Test void should_throw_when_request_fails() { var url = "https://github.com/foo/bar"; var encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); mockServer.addResponse("/api/v2/dop-translation/project-bindings?repositoryUrl=" + encodedUrl, new MockResponse.Builder().code(500).body("Internal error").build()); var throwable = catchThrowable(() -> underTest.getSQSProjectBindings(url, new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(ServerErrorException.class); } } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/push/PushApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push; import java.nio.file.Path; import java.time.Instant; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import mockwebserver3.MockResponse; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; class PushApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(true); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private PushApi underTest; @BeforeEach void setUp() { underTest = new PushApi(mockServer.serverApiHelper()); } @Test @Disabled("Settings will be supported later") void should_notify_setting_changed_event_for_simple_setting() { var mockResponse = new MockResponse.Builder() .body(""" event: SettingChanged data: { "projects": ["projectKey1", "projectKey2"], "key": "key1", "value": "value1" } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents) .extracting("projectKeys", "keyValues") .containsOnly(tuple(List.of("projectKey1", "projectKey2"), Map.of("key1", "value1"))); } @Test @Disabled("Settings will be supported later") void should_notify_setting_changed_event_for_multi_values_setting() { var mockResponse = new MockResponse.Builder() .body(""" event: SettingChanged data: { "projects": ["projectKey1", "projectKey2"], "key": "key1", "values": ["value1","value2"] } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents) .extracting("projectKeys", "keyValues") .containsOnly(tuple(List.of("projectKey1", "projectKey2"), Map.of("key1", "value1,value2"))); } @Test @Disabled("Settings will be supported later") void should_notify_setting_changed_event_for_field_values_setting() { var mockResponse = new MockResponse.Builder() .body(""" event: SettingChanged data: { "projects": ["projectKey1", "projectKey2"], "key": "key1", "fieldValues": [ { "key2": "value2", "key3": "value3" }, { "key4": "value4", "key5": "value5" } ] } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents) .extracting("projectKeys", "keyValues") .containsOnly(tuple( List.of("projectKey1", "projectKey2"), Map.of("key1", "1,2", "key1.1.key2", "value2", "key1.1.key3", "value3", "key1.2.key4", "value4", "key1.2.key5", "value5"))); } @Test void should_notify_rule_set_changed_event_without_impacts() { var mockResponse = new MockResponse.Builder() .body(""" event: RuleSetChanged data: {\ "projects": ["projectKey1", "projectKey2"],\ "activatedRules": [{\ "key": "java:S0000",\ "severity": "MAJOR",\ "params": [{\ "key": "key1",\ "value": "value1"\ }]\ }],\ "deactivatedRules": ["java:S4321"]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(receivedEvents) .extracting("projectKeys", "deactivatedRules") .containsOnly(tuple(List.of("projectKey1", "projectKey2"), List.of("java:S4321")))); assertThat(receivedEvents) .flatExtracting("activatedRules") .extracting("key", "severity") .containsOnly(tuple("java:S0000", IssueSeverity.MAJOR)); assertThat(receivedEvents) .flatExtracting("activatedRules") .extracting("parameters") .containsOnly(Map.of("key1", "value1")); } @Test void should_notify_rule_set_changed_event() { var mockResponse = new MockResponse.Builder() .body(""" event: RuleSetChanged data: {\ "projects": ["projectKey1", "projectKey2"],\ "activatedRules": [{\ "key": "java:S0000",\ "severity": "MAJOR",\ "params": [{\ "key": "key1",\ "value": "value1"\ }],\ "templateKey": "templateKey",\ "impacts": [{\ "softwareQuality": "SECURITY",\ "severity": "HIGH"\ }]\ }],\ "deactivatedRules": ["java:S4321"]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(receivedEvents) .extracting("projectKeys", "deactivatedRules") .containsOnly(tuple(List.of("projectKey1", "projectKey2"), List.of("java:S4321")))); assertThat(receivedEvents) .flatExtracting("activatedRules") .extracting("key", "severity") .containsOnly(tuple("java:S0000", IssueSeverity.MAJOR)); assertThat(receivedEvents) .flatExtracting("activatedRules") .extracting("parameters") .containsOnly(Map.of("key1", "value1")); } @Test void should_not_notify_while_event_is_incomplete() { var mockResponse = new MockResponse.Builder() .body(""" event: RuleSetChanged data: {\ "projects": ["projectKey1", "projectKey2"],\ "activatedRules": """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_ignore_events_without_project_keys() { var mockResponse = new MockResponse.Builder() .body(""" event: RuleSetChanged data: {\ "activatedRules": ["java:S1234"],\ "deactivatedRules": ["java:S4321"],\ "changedRules": [{\ "key": "java:S0000",\ "overriddenSeverity": "MAJOR",\ "params": [{\ "key": "key1",\ "value": "value1"\ }]\ }]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_ignore_unknown_events() { var mockResponse = new MockResponse.Builder() .body(""" event: UnknownEvent data: "plop" """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_ignore_ruleset_changed_events_with_invalid_json() { var mockResponse = new MockResponse.Builder() .body(""" event: RuleSetChanged data: {] """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_ignore_setting_changed_events_with_invalid_json() { var mockResponse = new MockResponse.Builder() .body(""" event: SettingChanged data: {] """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_ignore_invalid_setting_changed_events() { var mockResponse = new MockResponse.Builder() .body(""" event: SettingChanged data: {\ "projects": ["projectKey1", "projectKey2"],\ "key": "key1",\ "value": "",\ "values": [],\ "fieldValues": []\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1,projectKey2&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1", "projectKey2")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_notify_issue_changed_event_when_resolved_status_changed() { var mockResponse = new MockResponse.Builder() .body(""" event: IssueChanged data: {\ "projectKey": "projectKey1",\ "issues": [{\ "issueKey": "key1",\ "branchName": "master",\ "impacts": [ { "softwareQuality": "MAINTAINABILITY", "severity": "HIGH" } ]\ }],\ "resolved": true\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(receivedEvents) .asInstanceOf(InstanceOfAssertFactories.list(IssueChangedEvent.class)) .extracting(IssueChangedEvent::getResolved, IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType) .containsOnly(tuple(true, null, null)); assertThat(receivedEvents).isNotEmpty(); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues()).hasSize(1); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getIssueKey()).isEqualTo("key1"); }); } @Test void should_notify_issue_changed_event_when_severity_changed() { var mockResponse = new MockResponse.Builder() .body(""" event: IssueChanged data: {\ "projectKey": "projectKey1",\ "issues": [{\ "issueKey": "key1",\ "branchName": "master"\ }],\ "userSeverity": "MAJOR"\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(receivedEvents) .asInstanceOf(InstanceOfAssertFactories.list(IssueChangedEvent.class)) .extracting(IssueChangedEvent::getResolved, IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType) .containsOnly(tuple(null, IssueSeverity.MAJOR, null)); assertThat(receivedEvents).isNotEmpty(); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues()).hasSize(1); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getIssueKey()).isEqualTo("key1"); }); } @Test void should_notify_issue_changed_event_when_type_changed() { var mockResponse = new MockResponse.Builder() .body(""" event: IssueChanged data: {\ "projectKey": "projectKey1",\ "issues": [{\ "issueKey": "key1",\ "branchName": "master"\ }],\ "userType": "BUG"\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(receivedEvents) .asInstanceOf(InstanceOfAssertFactories.list(IssueChangedEvent.class)) .extracting(IssueChangedEvent::getResolved, IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType) .containsOnly(tuple(null, null, RuleType.BUG)); assertThat(receivedEvents).isNotEmpty(); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues()).hasSize(1); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getIssueKey()).isEqualTo("key1"); }); } @Test void should_not_notify_issue_changed_event_when_no_change_is_present() { var mockResponse = new MockResponse.Builder() .body(""" event: IssueChangedEvent data: {\ "projectKey": "projectKey1",\ "issues": [{\ "issueKey": "key1",\ "branchName": "master"\ }]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); assertThat(receivedEvents).isEmpty(); } @Test void should_notify_taint_vulnerability_raised_event() { var mockResponse = new MockResponse.Builder() .body(""" event: TaintVulnerabilityRaised data: {\ "key": "taintKey",\ "projectKey": "projectKey1",\ "branch": "branch",\ "creationDate": 123456789,\ "ruleKey": "javasecurity:S123",\ "severity": "MAJOR",\ "type": "VULNERABILITY",\ "mainLocation": {\ "filePath": "functions/taint.js",\ "message": "blah blah",\ "textRange": {\ "startLine": 17,\ "startLineOffset": 10,\ "endLine": 3,\ "endLineOffset": 2,\ "hash": "hash"\ }\ },\ "flows": [{\ "locations": [{\ "filePath": "functions/taint.js",\ "message": "sink: tainted value is used to perform a security-sensitive operation",\ "textRange": {\ "startLine": 17,\ "startLineOffset": 10,\ "endLine": 3,\ "endLineOffset": 2,\ "hash": "hash1"\ }\ },\ {\ "filePath": "functions/taint2.js",\ "message": "sink: tainted value is used to perform a security-sensitive operation",\ "textRange": {\ "startLine": 18,\ "startLineOffset": 11,\ "endLine": 4,\ "endLineOffset": 3,\ "hash": "hash2"\ }\ }]\ }]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(receivedEvents) .extracting("key", "projectKey", "branchName", "creationDate", "ruleKey", "severity", "type") .containsOnly(tuple("taintKey", "projectKey1", "branch", Instant.parse("1970-01-02T10:17:36.789Z"), "javasecurity:S123", IssueSeverity.MAJOR, RuleType.VULNERABILITY))); assertThat(receivedEvents) .extracting("mainLocation") .extracting("filePath", "message", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "textRange.hash") .containsOnly(tuple(Path.of("functions/taint.js"), "blah blah", 17, 10, 3, 2, "hash")); assertThat(receivedEvents) .flatExtracting("flows") .flatExtracting("locations") .extracting("filePath", "message", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "textRange.hash") .containsOnly( tuple(Path.of("functions/taint.js"), "sink: tainted value is used to perform a security-sensitive operation", 17, 10, 3, 2, "hash1"), tuple(Path.of("functions/taint2.js"), "sink: tainted value is used to perform a security-sensitive operation", 18, 11, 4, 3, "hash2")); } @Test void should_notify_taint_vulnerability_raised_event_with_cct() { var mockResponse = new MockResponse.Builder() .body(""" event: TaintVulnerabilityRaised data: {\ "key": "taintKey",\ "projectKey": "projectKey1",\ "branch": "branch",\ "creationDate": 123456789,\ "ruleKey": "javasecurity:S123",\ "severity": "MAJOR",\ "type": "VULNERABILITY",\ "cleanCodeAttribute": "TRUSTWORTHY",\ "impacts": [ { "softwareQuality": "SECURITY", "severity": "HIGH" } ],\ "type": "VULNERABILITY",\ "mainLocation": {\ "filePath": "functions/taint.js",\ "message": "blah blah",\ "textRange": {\ "startLine": 17,\ "startLineOffset": 10,\ "endLine": 3,\ "endLineOffset": 2,\ "hash": "hash"\ }\ },\ "flows": [{\ "locations": [{\ "filePath": "functions/taint.js",\ "message": "sink: tainted value is used to perform a security-sensitive operation",\ "textRange": {\ "startLine": 17,\ "startLineOffset": 10,\ "endLine": 3,\ "endLineOffset": 2,\ "hash": "hash1"\ }\ },\ {\ "filePath": "functions/taint2.js",\ "message": "sink: tainted value is used to perform a security-sensitive operation",\ "textRange": {\ "startLine": 18,\ "startLineOffset": 11,\ "endLine": 4,\ "endLineOffset": 3,\ "hash": "hash2"\ }\ }]\ }]\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(receivedEvents) .extracting("key", "projectKey", "branchName", "creationDate", "ruleKey", "severity", "type", "cleanCodeAttribute", "impacts") .containsOnly(tuple("taintKey", "projectKey1", "branch", Instant.parse("1970-01-02T10:17:36.789Z"), "javasecurity:S123", IssueSeverity.MAJOR, RuleType.VULNERABILITY, Optional.of(CleanCodeAttribute.TRUSTWORTHY), Map.of(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)))); } @Test void should_notify_taint_vulnerability_closed_event() { var mockResponse = new MockResponse.Builder() .body(""" event: TaintVulnerabilityClosed data: {\ "projectKey": "projectKey1",\ "key": "taintKey"\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(receivedEvents) .extracting("taintIssueKey") .containsOnly("taintKey")); } @Test void should_notify_issue_changed_event_when_software_impacts_changed() { var mockResponse = new MockResponse.Builder() .body(""" event: IssueChanged data: {\ "projectKey": "projectKey1",\ "issues": [{\ "issueKey": "key1",\ "branchName": "master",\ "impacts": [ { "softwareQuality": "MAINTAINABILITY", "severity": "HIGH" } ]\ }],\ "resolved": true\ } """) .build(); mockServer.addResponse("/api/push/sonarlint_events?projectKeys=projectKey1&languages=java,py", mockResponse); List receivedEvents = new CopyOnWriteArrayList<>(); underTest.subscribe(new LinkedHashSet<>(List.of("projectKey1")), new LinkedHashSet<>(List.of(SonarLanguage.JAVA, SonarLanguage.PYTHON)), receivedEvents::add); await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(receivedEvents) .asInstanceOf(InstanceOfAssertFactories.list(IssueChangedEvent.class)) .extracting(IssueChangedEvent::getResolved, IssueChangedEvent::getUserSeverity, IssueChangedEvent::getUserType) .containsOnly(tuple(true, null, null)); assertThat(receivedEvents).isNotEmpty(); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues()).hasSize(1); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getIssueKey()).isEqualTo("key1"); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getBranchName()).isEqualTo("master"); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getImpacts()).isNotEmpty(); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getImpacts()).containsKey(SoftwareQuality.MAINTAINABILITY); assertThat(((IssueChangedEvent) receivedEvents.get(0)).getImpactedIssues().get(0).getImpacts()).containsValue(ImpactSeverity.HIGH); }); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotChangedEventParserTest.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.commons.HotspotReviewStatus.ACKNOWLEDGED; import static org.sonarsource.sonarlint.core.commons.HotspotReviewStatus.SAFE; import static org.sonarsource.sonarlint.core.commons.HotspotReviewStatus.TO_REVIEW; class SecurityHotspotChangedEventParserTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); SecurityHotspotChangedEventParser parser = new SecurityHotspotChangedEventParser(); private static final String TEST_PAYLOAD_WITHOUT_KEY = """ { "projectKey": "test", "updateDate": 1685007187000, "status": "REVIEWED", "assignee": "AYfcq2moStCcBwCPm0uK", "resolution": "ACKNOWLEDGED", "filePath": "/project/path/to/file" }"""; private static final String TEST_PAYLOAD_WITHOUT_PROJECT_KEY = """ { "key": "AYhSN6mVrRF_krvNbHl1", "updateDate": 1685007187000, "status": "REVIEWED", "assignee": "AYfcq2moStCcBwCPm0uK", "resolution": "ACKNOWLEDGED", "filePath": "/project/path/to/file" }"""; private static final String TEST_PAYLOAD_WITHOUT_FILE_PATH = """ { "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "updateDate": 1685007187000, "status": "REVIEWED", "assignee": "AYfcq2moStCcBwCPm0uK", "resolution": "ACKNOWLEDGED" }"""; private static final String VALID_PAYLOAD = """ { "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "updateDate": 1685007187000, "status": "REVIEWED", "assignee": "assigneeEmail", "resolution": "ACKNOWLEDGED", "filePath": "/project/path/to/file" }"""; @ParameterizedTest @ValueSource(strings = {TEST_PAYLOAD_WITHOUT_KEY, TEST_PAYLOAD_WITHOUT_PROJECT_KEY, TEST_PAYLOAD_WITHOUT_FILE_PATH}) void shouldReturnEmptyOptionalWhenPayloadIsInvalid(String invalidPayload) { var parseResult = parser.parse(invalidPayload); assertThat(parseResult).isEmpty(); } @Test void shouldReturnChangeEventWhenPayloadIsValid() { var parsedResult = parser.parse(VALID_PAYLOAD); assertThat(parsedResult).isPresent(); assertThat(parsedResult.get().getAssignee()).isEqualTo("assigneeEmail"); assertThat(parsedResult.get().getHotspotKey()).isEqualTo("AYhSN6mVrRF_krvNbHl1"); assertThat(parsedResult.get().getStatus()).isEqualTo(ACKNOWLEDGED); assertThat(parsedResult.get().getProjectKey()).isEqualTo("test"); assertThat(parsedResult.get().getFilePath()).isEqualTo(Path.of("/project/path/to/file")); } @Test void shouldCorrectlyMapStatus() { var payloadNoResolution = """ { "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "updateDate": 1685007187000, "status": "TO_REVIEW", "assignee": "assigneeEmail", "filePath": "/project/path/to/file" }"""; var parsedResult = parser.parse(payloadNoResolution); assertThat(parsedResult).isPresent(); assertThat(parsedResult.get().getStatus()).isEqualTo(TO_REVIEW); var payloadSafe = """ { "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "updateDate": 1685007187000, "status": "REVIEWED", "assignee": "assigneeEmail", "resolution": "SAFE", "filePath": "/project/path/to/file" }"""; var parsedResult2 = parser.parse(payloadSafe); assertThat(parsedResult2).isPresent(); assertThat(parsedResult2.get().getStatus()).isEqualTo(SAFE); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotClosedEventParserTest.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class SecurityHotspotClosedEventParserTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); SecurityHotspotClosedEventParser parser = new SecurityHotspotClosedEventParser(); private static final String TEST_PAYLOAD_WITHOUT_KEY = """ { "projectKey": "test", "filePath": "/project/path/to/file" }"""; private static final String TEST_PAYLOAD_WITHOUT_PROJECT_KEY = """ { "key": "AYhSN6mVrRF_krvNbHl1", "filePath": "/project/path/to/file" }"""; private static final String VALID_PAYLOAD = """ { "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "filePath": "/project/path/to/file" }"""; @ParameterizedTest @ValueSource(strings = {TEST_PAYLOAD_WITHOUT_KEY, TEST_PAYLOAD_WITHOUT_PROJECT_KEY}) void shouldReturnEmptyOptionalWhenPayloadIsInvalid(String invalidPayload) { var parseResult = parser.parse(invalidPayload); assertThat(parseResult).isEmpty(); } @Test void shouldReturnChangeEventWhenPayloadIsValid() { var parsedResult = parser.parse(VALID_PAYLOAD); assertThat(parsedResult).isPresent(); assertThat(parsedResult.get().getHotspotKey()).isEqualTo("AYhSN6mVrRF_krvNbHl1"); assertThat(parsedResult.get().getProjectKey()).isEqualTo("test"); assertThat(parsedResult.get().getFilePath()).isEqualTo(Path.of("/project/path/to/file")); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/push/parsing/SecurityHotspotRaisedEventParserTest.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.push.parsing; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; class SecurityHotspotRaisedEventParserTest { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); SecurityHotspotRaisedEventParser parser = new SecurityHotspotRaisedEventParser(); private static final String TEST_PAYLOAD_WITHOUT_KEY = """ { "status": "TO_REVIEW", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "filePath": "src/main/java/org/example/Main.java", "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "projectKey": "test", "branch": "some-branch" }"""; private static final String TEST_PAYLOAD_WITHOUT_BRANCH = """ { "status": "TO_REVIEW", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "filePath": "src/main/java/org/example/Main.java", "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test" }"""; private static final String TEST_PAYLOAD_WITHOUT_PROJECT_KEY = """ { "status": "TO_REVIEW", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "filePath": "src/main/java/org/example/Main.java", "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "key": "AYhSN6mVrRF_krvNbHl1", "branch": "some-branch" }"""; private static final String TEST_PAYLOAD_WITHOUT_FILE_PATH = """ { "status": "TO_REVIEW", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "branch": "some-branch" }"""; private static final String VALID_PAYLOAD = """ { "status": "TO_REVIEW", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "filePath": "src/main/java/org/example/Main.java", "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "branch": "some-branch" }"""; private static final String VALID_PAYLOAD_REVIEWED = """ { "status": "REVIEWED", "vulnerabilityProbability": "MEDIUM", "creationDate": 1685006550000, "mainLocation": { "filePath": "src/main/java/org/example/Main.java", "message": "Make sure that using this pseudorandom number generator is safe here.", "textRange": { "startLine": 12, "startLineOffset": 29, "endLine": 12, "endLineOffset": 36, "hash": "43b5c9175984c071f30b873fdce0a000" } }, "ruleKey": "java:S2245", "key": "AYhSN6mVrRF_krvNbHl1", "projectKey": "test", "branch": "some-branch" }"""; @ParameterizedTest @ValueSource(strings = {TEST_PAYLOAD_WITHOUT_KEY, TEST_PAYLOAD_WITHOUT_PROJECT_KEY, TEST_PAYLOAD_WITHOUT_FILE_PATH, TEST_PAYLOAD_WITHOUT_BRANCH}) void shouldReturnEmptyOptionalWhenPayloadIsInvalid(String invalidPayload) { var parseResult = parser.parse(invalidPayload); assertThat(parseResult).isEmpty(); } @Test void shouldReturnChangeEventWhenPayloadIsValid() { var parsedResult = parser.parse(VALID_PAYLOAD); assertThat(parsedResult).isPresent(); assertThat(parsedResult.get().getHotspotKey()).isEqualTo("AYhSN6mVrRF_krvNbHl1"); assertThat(parsedResult.get().getStatus()).isEqualTo(HotspotReviewStatus.TO_REVIEW); assertThat(parsedResult.get().getProjectKey()).isEqualTo("test"); assertThat(parsedResult.get().getMainLocation().getFilePath()).isEqualTo(Path.of("src/main/java/org/example/Main.java")); assertThat(parsedResult.get().getBranch()).isEqualTo("some-branch"); assertThat(parsedResult.get().getRuleKey()).isEqualTo("java:S2245"); assertThat(parsedResult.get().getMainLocation().getMessage()).isEqualTo("Make sure that using this pseudorandom number generator is safe here."); } @Test void shouldReturnChangeEventWhenPayloadIsValidAndHotspotIsReviewed() { var parsedResult = parser.parse(VALID_PAYLOAD_REVIEWED); assertThat(parsedResult).isPresent(); assertThat(parsedResult.get().getHotspotKey()).isEqualTo("AYhSN6mVrRF_krvNbHl1"); assertThat(parsedResult.get().getStatus()).isEqualTo(HotspotReviewStatus.SAFE); assertThat(parsedResult.get().getProjectKey()).isEqualTo("test"); assertThat(parsedResult.get().getMainLocation().getFilePath()).isEqualTo(Path.of("src/main/java/org/example/Main.java")); assertThat(parsedResult.get().getBranch()).isEqualTo("some-branch"); assertThat(parsedResult.get().getRuleKey()).isEqualTo("java:S2245"); assertThat(parsedResult.get().getMainLocation().getMessage()).isEqualTo("Make sure that using this pseudorandom number generator is safe here."); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/qualityprofile/QualityProfileApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.qualityprofile; import mockwebserver3.MockResponse; import okhttp3.Headers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.exception.ProjectNotFoundException; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Qualityprofiles; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertThrows; class QualityProfileApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @Test void should_throw_when_the_endpoint_is_not_found() { var underTest = new QualityProfileApi(mockServer.serverApiHelper()); mockServer.addResponse("/api/qualityprofiles/search.protobuf?project=projectKey", new MockResponse(404, Headers.EMPTY, "")); var cancelMonitor = new SonarLintCancelMonitor(); assertThrows(ProjectNotFoundException.class, () -> underTest.getQualityProfiles("projectKey", cancelMonitor)); } @Test void should_throw_when_a_server_error_occurs() { var underTest = new QualityProfileApi(mockServer.serverApiHelper()); mockServer.addResponse("/api/qualityprofiles/search.protobuf?project=projectKey", new MockResponse(503, Headers.EMPTY, "")); var cancelMonitor = new SonarLintCancelMonitor(); assertThrows(ServerErrorException.class, () -> underTest.getQualityProfiles("projectKey", cancelMonitor)); } @Test void should_return_the_quality_profiles_of_a_given_project() { var underTest = new QualityProfileApi(mockServer.serverApiHelper()); mockServer.addProtobufResponse("/api/qualityprofiles/search.protobuf?project=projectKey", Qualityprofiles.SearchWsResponse.newBuilder() .addProfiles(Qualityprofiles.SearchWsResponse.QualityProfile.newBuilder() .setIsDefault(true) .setKey("profileKey") .setName("profileName") .setLanguage("lang") .setLanguageName("langName") .setActiveRuleCount(12) .setRulesUpdatedAt("rulesUpdatedAt") .setUserUpdatedAt("userUpdatedAt") .build()) .build()); var qualityProfiles = underTest.getQualityProfiles("projectKey", new SonarLintCancelMonitor()); assertThat(qualityProfiles) .extracting("default", "key", "name", "language", "languageName", "activeRuleCount", "rulesUpdatedAt", "userUpdatedAt") .containsOnly(tuple(true, "profileKey", "profileName", "lang", "langName", 12L, "rulesUpdatedAt", "userUpdatedAt")); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/rules/RulesApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.rules; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Rules; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class RulesApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @Test void logErrorParsingRuleDescription() { mockServer.addStringResponse("/api/rules/show.protobuf?key=java:S1234", "trash"); var rulesApi = new RulesApi(mockServer.serverApiHelper()); assertThat(rulesApi.getRule("java:S1234", new SonarLintCancelMonitor())).isEmpty(); assertThat(logTester.logs()).contains("Error when fetching rule 'java:S1234'"); } @Test void should_get_rule() { mockServer.addProtobufResponse("/api/rules/show.protobuf?key=java:S1234", Rules.ShowResponse.newBuilder().setRule( Rules.Rule.newBuilder() .setName("name") .setSeverity("MINOR") .setType(Common.RuleType.VULNERABILITY) .setLang(SonarLanguage.PYTHON.getSonarLanguageKey()) .setHtmlNote("htmlNote") .setDescriptionSections(Rules.Rule.DescriptionSections.newBuilder() .addDescriptionSections(Rules.Rule.DescriptionSection.newBuilder() .setKey("default") .setContent("desc") .build()) .build()) .setCleanCodeAttribute(Common.CleanCodeAttribute.COMPLETE) .setImpacts(Rules.Rule.Impacts.newBuilder().addImpacts(Common.Impact.newBuilder().setSeverity(Common.ImpactSeverity.HIGH).setSoftwareQuality(Common.SoftwareQuality.MAINTAINABILITY).build()).build()) .build()) .build()); var rulesApi = new RulesApi(mockServer.serverApiHelper()); var rule = rulesApi.getRule("java:S1234", new SonarLintCancelMonitor()).get(); assertThat(rule).extracting("name", "severity", "type", "language", "htmlNote", "cleanCodeAttribute", "impacts") .contains("name", IssueSeverity.MINOR, RuleType.VULNERABILITY, SonarLanguage.PYTHON, "htmlNote", CleanCodeAttribute.COMPLETE, Map.of(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH)); assertThat(rule.getDescriptionSections().get(0).getHtmlContent()).isEqualTo("desc"); } @Test void should_get_rule_with_description_sections() { mockServer.addProtobufResponse("/api/rules/show.protobuf?key=java:S1234", Rules.ShowResponse.newBuilder().setRule( Rules.Rule.newBuilder() .setName("name") .setSeverity("MINOR") .setType(Common.RuleType.VULNERABILITY) .setLang(SonarLanguage.PYTHON.getSonarLanguageKey()) .setDescriptionSections(Rules.Rule.DescriptionSections.newBuilder() .addDescriptionSections(Rules.Rule.DescriptionSection.newBuilder().setKey("sectionKey").setContent("htmlContent").build()) .addDescriptionSections( Rules.Rule.DescriptionSection.newBuilder().setKey("sectionKey2").setContent("htmlContent2").setContext(Rules.Rule.DescriptionSection.Context.newBuilder() .setKey("contextKey").setDisplayName("displayName").build()).build()) .build()) .setHtmlNote("htmlNote") .setCleanCodeAttribute(Common.CleanCodeAttribute.CONVENTIONAL) .setImpacts(Rules.Rule.Impacts.newBuilder().addImpacts(Common.Impact.newBuilder().setSeverity(Common.ImpactSeverity.LOW).setSoftwareQuality(Common.SoftwareQuality.RELIABILITY).build()).build()) .build()) .build()); var rulesApi = new RulesApi(mockServer.serverApiHelper()); var rule = rulesApi.getRule("java:S1234", new SonarLintCancelMonitor()).get(); assertThat(rule).extracting("name", "severity", "type", "language","htmlNote", "cleanCodeAttribute", "impacts") .contains("name", IssueSeverity.MINOR, RuleType.VULNERABILITY, SonarLanguage.PYTHON, "htmlNote", CleanCodeAttribute.CONVENTIONAL, Map.of(SoftwareQuality.RELIABILITY, ImpactSeverity.LOW)); var sections = rule.getDescriptionSections(); assertThat(sections).hasSize(2); assertThat(sections.get(0)).extracting("key", "htmlContent", "context") .containsExactly("sectionKey", "htmlContent", Optional.empty()); assertThat(sections.get(1)).extracting("key", "htmlContent") .containsExactly("sectionKey2", "htmlContent2"); assertThat(sections.get(1).getContext()).hasValueSatisfying(context -> { assertThat(context.getKey()).isEqualTo("contextKey"); assertThat(context.getDisplayName()).isEqualTo("displayName"); }); } @Test void should_get_rule_from_organization() { mockServer.addProtobufResponse("/api/rules/show.protobuf?key=java:S1234&organization=orgKey", Rules.ShowResponse.newBuilder().setRule( Rules.Rule.newBuilder() .setName("name") .setSeverity("MAJOR") .setType(Common.RuleType.VULNERABILITY) .setLang(SonarLanguage.PYTHON.getSonarLanguageKey()) .setHtmlNote("htmlNote") .setDescriptionSections(Rules.Rule.DescriptionSections.newBuilder() .addDescriptionSections(Rules.Rule.DescriptionSection.newBuilder() .setKey("default") .setContent("desc") .build()) .build()) .build()) .build()); var rulesApi = new RulesApi(mockServer.serverApiHelper("orgKey")); var rule = rulesApi.getRule("java:S1234", new SonarLintCancelMonitor()).get(); assertThat(rule).extracting("name", "severity", "type", "language", "htmlNote") .contains("name", IssueSeverity.MAJOR, RuleType.VULNERABILITY, SonarLanguage.PYTHON, "htmlNote"); assertThat(rule.getDescriptionSections().get(0).getHtmlContent()).isEqualTo("desc"); } @Test void should_get_active_rules_of_a_given_quality_profile() { mockServer.addProtobufResponse( "/api/rules/search.protobuf?qprofile=QPKEY%2B&organization=orgKey&activation=true&f=templateKey,actives&types=CODE_SMELL,BUG,VULNERABILITY,SECURITY_HOTSPOT&s=key&ps=500&p=1", Rules.SearchResponse.newBuilder() .setPaging(Common.Paging.newBuilder().setTotal(2).build()) .addRules(Rules.Rule.newBuilder().setKey("repo:key_with_template").setTemplateKey("template").build()) .addRules(Rules.Rule.newBuilder().setKey("repo:key").build()) .setActives( Rules.Actives.newBuilder() .putActives("repo:key_with_template", Rules.ActiveList.newBuilder().addActiveList( Rules.Active.newBuilder() .setSeverity("MAJOR") .addParams(Rules.Active.Param.newBuilder().setKey("paramKey").setValue("paramValue").build()) .build()) .build()) .putActives("repo:key", Rules.ActiveList.newBuilder().addActiveList( Rules.Active.newBuilder() .setSeverity("MINOR") .build()) .build()) .build()) .build()); var rulesApi = new RulesApi(mockServer.serverApiHelper("orgKey")); var activeRules = rulesApi.getAllActiveRules("QPKEY+", new SonarLintCancelMonitor()); assertThat(activeRules) .extracting("ruleKey", "severity", "templateKey", "params") .containsOnly(tuple("repo:key", IssueSeverity.MINOR, "", Map.of()), tuple("repo:key_with_template", IssueSeverity.MAJOR, "template", Map.of("paramKey", "paramValue"))); } @Test void should_fallback_on_deprecated_pagination_for_sonarqube_older_than_9_8() { mockServer.addProtobufResponse( "/api/rules/search.protobuf?qprofile=QPKEY%2B&organization=orgKey&activation=true&f=templateKey,actives&types=CODE_SMELL,BUG,VULNERABILITY,SECURITY_HOTSPOT&s=key&ps=500&p=1", Rules.SearchResponse.newBuilder() .setTotal(501) .addRules(Rules.Rule.newBuilder().setKey("repo:key1").build()) .setActives( Rules.Actives.newBuilder() .putActives("repo:key1", Rules.ActiveList.newBuilder().addActiveList( Rules.Active.newBuilder() .setSeverity("MINOR") .build()) .build()) .build()) .build()); mockServer.addProtobufResponse( "/api/rules/search.protobuf?qprofile=QPKEY%2B&organization=orgKey&activation=true&f=templateKey,actives&types=CODE_SMELL,BUG,VULNERABILITY,SECURITY_HOTSPOT&s=key&ps=500&p=2", Rules.SearchResponse.newBuilder() .setTotal(501) .addRules(Rules.Rule.newBuilder().setKey("repo:key2").build()) .setActives( Rules.Actives.newBuilder() .putActives("repo:key2", Rules.ActiveList.newBuilder().addActiveList( Rules.Active.newBuilder() .setSeverity("MAJOR") .build()) .build()) .build()) .build()); var rulesApi = new RulesApi(mockServer.serverApiHelper("orgKey")); var activeRules = rulesApi.getAllActiveRules("QPKEY+", new SonarLintCancelMonitor()); assertThat(activeRules).extracting(ServerActiveRule::getRuleKey).containsExactlyInAnyOrder("repo:key1", "repo:key2"); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/sca/ScaApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.sca; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ScaApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private static final String EMPTY_ISSUES_RELEASES_JSON = """ { "issuesReleases": [], "page": { "pageIndex": 1, "pageSize": 100, "total": 0 } } """; private ScaApi scaApi; @Nested class SonarQubeServer { @BeforeEach void prepare() { scaApi = new ScaApi(mockServer.serverApiHelper()); } @Test void should_get_issues_releases_with_empty_response() { mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=my-project&branchKey=main&pageSize=500&pageIndex=1", EMPTY_ISSUES_RELEASES_JSON); var response = scaApi.getIssuesReleases("my-project", "main", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).isEmpty(); } @Test void should_get_issues_releases_of_vulnerability_type() { var uuid = UUID.randomUUID(); var jsonResponse = String.format(""" { "issuesReleases": [ { "key": "%s", "type": "VULNERABILITY", "severity": "HIGH", "quality": "MAINTAINABILITY", "vulnerabilityId": "CVE-2023-12345", "cvssScore": "7.5", "release": { "packageName": "com.example.vulnerable", "version": "1.0.0" }, "transitions": ["CONFIRM", "REOPEN"] } ], "page": { "pageIndex": 1, "pageSize": 100, "total": 1 } } """, uuid); mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=test-project&branchKey=feature%2Fmy-branch&pageSize=500&pageIndex=1", jsonResponse); var response = scaApi.getIssuesReleases("test-project", "feature/my-branch", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).hasSize(1); var issueRelease = response.issuesReleases().get(0); assertThat(issueRelease.key()).isEqualTo(uuid); assertThat(issueRelease.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.VULNERABILITY); assertThat(issueRelease.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.HIGH); assertThat(issueRelease.quality()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.SoftwareQuality.MAINTAINABILITY); assertThat(issueRelease.vulnerabilityId()).isEqualTo("CVE-2023-12345"); assertThat(issueRelease.cvssScore()).isEqualTo("7.5"); assertThat(issueRelease.release().packageName()).isEqualTo("com.example.vulnerable"); assertThat(issueRelease.release().version()).isEqualTo("1.0.0"); assertThat(issueRelease.transitions()).containsExactly( GetIssuesReleasesResponse.IssuesRelease.Transition.CONFIRM, GetIssuesReleasesResponse.IssuesRelease.Transition.REOPEN); } @Test void should_get_issues_releases_of_prohibited_license_type() { var uuid = UUID.randomUUID(); var jsonResponse = String.format(""" { "issuesReleases": [ { "key": "%s", "type": "PROHIBITED_LICENSE", "severity": "BLOCKER", "quality": "SECURITY", "vulnerabilityId": null, "cvssScore": null, "release": { "packageName": "com.example.prohibited", "version": "2.1.0" }, "transitions": ["ACCEPT", "SAFE"] } ], "page": { "pageIndex": 1, "pageSize": 100, "total": 1 } } """, uuid); mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=license-project&branchKey=develop&pageSize=500&pageIndex=1", jsonResponse); var response = scaApi.getIssuesReleases("license-project", "develop", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).hasSize(1); var issueRelease = response.issuesReleases().get(0); assertThat(issueRelease.key()).isEqualTo(uuid); assertThat(issueRelease.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.PROHIBITED_LICENSE); assertThat(issueRelease.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.BLOCKER); assertThat(issueRelease.quality()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.SoftwareQuality.SECURITY); assertThat(issueRelease.vulnerabilityId()).isNull(); assertThat(issueRelease.cvssScore()).isNull(); assertThat(issueRelease.release().packageName()).isEqualTo("com.example.prohibited"); assertThat(issueRelease.release().version()).isEqualTo("2.1.0"); assertThat(issueRelease.transitions()).containsExactly( GetIssuesReleasesResponse.IssuesRelease.Transition.ACCEPT, GetIssuesReleasesResponse.IssuesRelease.Transition.SAFE); } @Test void should_get_issues_releases_with_multiple_issues() { var uuid1 = UUID.randomUUID(); var uuid2 = UUID.randomUUID(); var jsonResponse = String.format(""" { "issuesReleases": [ { "key": "%s", "type": "VULNERABILITY", "severity": "MEDIUM", "quality": "RELIABILITY", "vulnerabilityId": "CVE-2023-12345", "cvssScore": "7.5", "release": { "packageName": "com.example.first", "version": "1.0.0" }, "transitions": ["CONFIRM"] }, { "key": "%s", "type": "PROHIBITED_LICENSE", "severity": "LOW", "quality": "MAINTAINABILITY", "vulnerabilityId": null, "cvssScore": null, "release": { "packageName": "com.example.second", "version": "2.0.0" }, "transitions": ["ACCEPT", "SAFE", "FIXED"] } ], "page": { "pageIndex": 1, "pageSize": 100, "total": 2 } } """, uuid1, uuid2); mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=multi-project&branchKey=master&pageSize=500&pageIndex=1", jsonResponse); var response = scaApi.getIssuesReleases("multi-project", "master", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).hasSize(2); var firstIssue = response.issuesReleases().get(0); assertThat(firstIssue.key()).isEqualTo(uuid1); assertThat(firstIssue.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.VULNERABILITY); assertThat(firstIssue.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.MEDIUM); assertThat(firstIssue.quality()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.SoftwareQuality.RELIABILITY); assertThat(firstIssue.vulnerabilityId()).isEqualTo("CVE-2023-12345"); assertThat(firstIssue.cvssScore()).isEqualTo("7.5"); assertThat(firstIssue.release().packageName()).isEqualTo("com.example.first"); assertThat(firstIssue.release().version()).isEqualTo("1.0.0"); assertThat(firstIssue.transitions()).containsExactly(GetIssuesReleasesResponse.IssuesRelease.Transition.CONFIRM); var secondIssue = response.issuesReleases().get(1); assertThat(secondIssue.key()).isEqualTo(uuid2); assertThat(secondIssue.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.PROHIBITED_LICENSE); assertThat(secondIssue.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.LOW); assertThat(secondIssue.quality()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.SoftwareQuality.MAINTAINABILITY); assertThat(secondIssue.vulnerabilityId()).isNull(); assertThat(secondIssue.cvssScore()).isNull(); assertThat(secondIssue.release().packageName()).isEqualTo("com.example.second"); assertThat(secondIssue.release().version()).isEqualTo("2.0.0"); assertThat(secondIssue.transitions()).containsExactly( GetIssuesReleasesResponse.IssuesRelease.Transition.ACCEPT, GetIssuesReleasesResponse.IssuesRelease.Transition.SAFE, GetIssuesReleasesResponse.IssuesRelease.Transition.FIXED); } @Test void should_handle_special_characters_in_project_key_and_branch_name() { mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=my%3Aproject%2Bkey&branchKey=feature%2Fmy-branch%3Atest&pageSize=500&pageIndex=1", EMPTY_ISSUES_RELEASES_JSON); var response = scaApi.getIssuesReleases("my:project+key", "feature/my-branch:test", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).isEmpty(); } @Test void should_handle_malformed_json_response() { mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=test&branchName=main&pageSize=500&pageIndex=1", "invalid json"); assertThatThrownBy(() -> scaApi.getIssuesReleases("test", "main", new SonarLintCancelMonitor())) .isInstanceOf(Exception.class); } @Test void should_handle_empty_transitions() { var uuid = UUID.randomUUID(); var jsonResponse = String.format(""" { "issuesReleases": [ { "key": "%s", "type": "VULNERABILITY", "severity": "INFO", "release": { "packageName": "com.example.minimal", "version": "0.1.0" }, "transitions": [] } ], "page": { "pageIndex": 1, "pageSize": 100, "total": 1 } } """, uuid); mockServer.addStringResponse("/api/v2/sca/issues-releases?projectKey=minimal-project&branchKey=main&pageSize=500&pageIndex=1", jsonResponse); var response = scaApi.getIssuesReleases("minimal-project", "main", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).hasSize(1); var issueRelease = response.issuesReleases().get(0); assertThat(issueRelease.key()).isEqualTo(uuid); assertThat(issueRelease.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.VULNERABILITY); assertThat(issueRelease.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.INFO); assertThat(issueRelease.transitions()).isEmpty(); } } @Nested class SonarQubeCloud { @BeforeEach void prepare() { scaApi = new ScaApi(mockServer.serverApiHelper("orgKey")); } @Test void should_get_issues_releases_with_empty_response() { mockServer.addStringResponse("/sca/issues-releases?projectKey=my-project&branchKey=main&pageSize=500&pageIndex=1", EMPTY_ISSUES_RELEASES_JSON); var response = scaApi.getIssuesReleases("my-project", "main", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).isEmpty(); } @Test void should_get_issues_releases_of_vulnerability_type() { var uuid = UUID.randomUUID(); var jsonResponse = String.format(""" { "issuesReleases": [ { "key": "%s", "type": "VULNERABILITY", "severity": "HIGH", "quality": "MAINTAINABILITY", "vulnerabilityId": "CVE-2023-12345", "cvssScore": "7.5", "release": { "packageName": "com.example.vulnerable", "version": "1.0.0" }, "transitions": ["CONFIRM", "REOPEN"] } ], "page": { "pageIndex": 1, "pageSize": 100, "total": 1 } } """, uuid); mockServer.addStringResponse("/sca/issues-releases?projectKey=test-project&branchKey=feature%2Fmy-branch&pageSize=500&pageIndex=1", jsonResponse); var response = scaApi.getIssuesReleases("test-project", "feature/my-branch", new SonarLintCancelMonitor()); assertThat(response.issuesReleases()).hasSize(1); var issueRelease = response.issuesReleases().get(0); assertThat(issueRelease.key()).isEqualTo(uuid); assertThat(issueRelease.type()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Type.VULNERABILITY); assertThat(issueRelease.severity()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.Severity.HIGH); assertThat(issueRelease.quality()).isEqualTo(GetIssuesReleasesResponse.IssuesRelease.SoftwareQuality.MAINTAINABILITY); assertThat(issueRelease.vulnerabilityId()).isEqualTo("CVE-2023-12345"); assertThat(issueRelease.cvssScore()).isEqualTo("7.5"); assertThat(issueRelease.release().packageName()).isEqualTo("com.example.vulnerable"); assertThat(issueRelease.release().version()).isEqualTo("1.0.0"); assertThat(issueRelease.transitions()).containsExactly( GetIssuesReleasesResponse.IssuesRelease.Transition.CONFIRM, GetIssuesReleasesResponse.IssuesRelease.Transition.REOPEN); } } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/settings/SettingsApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.settings; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Settings; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; class SettingsApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @Test void test_fetch_project_settings() { var valuesBuilder = Settings.FieldValues.Value.newBuilder(); valuesBuilder.putValue("filepattern", "**/*.xml"); valuesBuilder.putValue("rulepattern", "*:S12345"); var value1 = valuesBuilder.build(); valuesBuilder.clear(); valuesBuilder.putValue("filepattern", "**/*.java"); valuesBuilder.putValue("rulepattern", "*:S456"); var value2 = valuesBuilder.build(); var response = Settings.ValuesWsResponse.newBuilder() .addSettings(Settings.Setting.newBuilder() .setKey("sonar.inclusions") .setValues(Settings.Values.newBuilder().addValues("**/*.java"))) .addSettings(Settings.Setting.newBuilder() .setKey("sonar.java.fileSuffixes") .setValue("*.java")) .addSettings(Settings.Setting.newBuilder() .setKey("sonar.issue.exclusions.multicriteria") .setFieldValues(Settings.FieldValues.newBuilder().addFieldValues(value1).addFieldValues(value2)).build()) .build(); mockServer.addProtobufResponse("/api/settings/values.protobuf?component=foo", response); var projectSettings = new SettingsApi(mockServer.serverApiHelper()).getProjectSettings("foo", new SonarLintCancelMonitor()); assertThat(projectSettings).containsOnly( entry("sonar.inclusions", "**/*.java"), entry("sonar.java.fileSuffixes", "*.java"), entry("sonar.issue.exclusions.multicriteria", "1,2"), entry("sonar.issue.exclusions.multicriteria.1.filepattern", "**/*.xml"), entry("sonar.issue.exclusions.multicriteria.1.rulepattern", "*:S12345"), entry("sonar.issue.exclusions.multicriteria.2.filepattern", "**/*.java"), entry("sonar.issue.exclusions.multicriteria.2.rulepattern", "*:S456")); } @Test void test_fetch_global_setting() { var response = Settings.ValuesWsResponse.newBuilder() .addSettings(Settings.Setting.newBuilder() .setKey("sonar.multi-quality-mode.enabled") .setValue("true")) .addSettings(Settings.Setting.newBuilder() .setKey("fake.property") .setValue("false")) .build(); mockServer.addProtobufResponse("/api/settings/values.protobuf", response); var globalSettings = new SettingsApi(mockServer.serverApiHelper()).getGlobalSettings(new SonarLintCancelMonitor()); assertThat(globalSettings).isEqualTo(Map.of("sonar.multi-quality-mode.enabled", "true", "fake.property", "false")); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/stream/EventStreamTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.stream; import java.util.ArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpConnectionListener; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class EventStreamTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final ServerApiHelper apiHelper = mock(ServerApiHelper.class); private final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); private final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(HttpConnectionListener.class); private final ArgumentCaptor> consumerCaptor = ArgumentCaptor.forClass(Consumer.class); private final ArrayList receivedEvents = new ArrayList<>(); private EventStream stream; @BeforeEach void setUp() { stream = new EventStream(apiHelper, receivedEvents::add, executor); } @Test void should_log_when_connected() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onConnected(); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Connected to server event-stream"); } @Test void should_notify_consumer_when_event_received() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), consumerCaptor.capture()); var scheduledFuture = mock(ScheduledFuture.class); when(executor.schedule(any(Runnable.class), anyLong(), any())).thenReturn(scheduledFuture); listenerCaptor.getValue().onConnected(); consumerCaptor.getValue().accept("event: type\ndata: data\n\n"); assertThat(receivedEvents) .extracting("type", "data") .containsOnly(tuple("type", "data")); } @Test void should_not_retry_when_unauthorized() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(401); verifyNoInteractions(executor); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Cannot connect to server event-stream, unauthorized"); } @Test void should_not_retry_when_forbidden() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(403); verifyNoInteractions(executor); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Cannot connect to server event-stream, forbidden"); } @Test void should_not_retry_when_api_not_found() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(404); verifyNoInteractions(executor); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Server events not supported by the server"); } @Test void should_retry_when_server_error() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(500); verify(executor).schedule(any(Runnable.class), eq(60L), eq(TimeUnit.SECONDS)); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Cannot connect to server event-stream (500), retrying in 60s"); } @Test void should_reconnect_when_disconnected() { stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); var listener = listenerCaptor.getValue(); var scheduledFuture = mock(ScheduledFuture.class); when(executor.schedule(any(Runnable.class), anyLong(), any())).thenReturn(scheduledFuture); listener.onConnected(); listener.onClosed(); verify(apiHelper).getEventStream(eq("wsPath"), eq(listener), any()); assertThat(logTester.logs()) .containsOnly( "Connecting to server event-stream at 'wsPath'...", "Connected to server event-stream", "Disconnected from server event-stream, reconnecting now", "Connecting to server event-stream at 'wsPath'..."); } @Test void should_stop_retrying_after_failed_attempts() { stream.connect("wsPath"); for (int attemptNumber = 0; attemptNumber < 9; attemptNumber++) { verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(null); ArgumentCaptor callableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(executor).schedule(callableCaptor.capture(), anyLong(), any()); clearInvocations(executor); clearInvocations(apiHelper); callableCaptor.getValue().run(); } verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(null); verifyNoInteractions(executor); assertThat(logTester.logs()) .contains( "Connecting to server event-stream at 'wsPath'...", "Cannot connect to server event-stream, retrying in 15360s", "Cannot connect to server event-stream, stop retrying"); } @Test void should_reconnect_when_no_heart_beat_received_for_a_while() { var scheduledFuture = mock(ScheduledFuture.class); ArgumentCaptor callableCaptor = ArgumentCaptor.forClass(Runnable.class); when(executor.schedule(callableCaptor.capture(), anyLong(), any())).thenReturn(scheduledFuture); stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); var listener = listenerCaptor.getValue(); listener.onConnected(); callableCaptor.getValue().run(); verify(apiHelper).getEventStream(eq("wsPath"), eq(listener), any()); } @Test void should_cancel_request_when_closing_stream() { var asyncRequest = mock(HttpClient.AsyncRequest.class); when(apiHelper.getEventStream(eq("wsPath"), any(), any())).thenReturn(asyncRequest); stream.connect("wsPath"); stream.close(); verify(asyncRequest).cancel(); } @Test void should_cancel_delayed_retry_when_closing_stream() { var scheduledFuture = mock(ScheduledFuture.class); when(executor.schedule(any(Runnable.class), anyLong(), any())).thenReturn(scheduledFuture); stream.connect("wsPath"); verify(apiHelper).getEventStream(eq("wsPath"), listenerCaptor.capture(), any()); listenerCaptor.getValue().onError(null); stream.close(); verify(scheduledFuture).cancel(true); } @Test void should_close_executor_when_closing_stream() { stream.close(); verify(executor).shutdownNow(); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/users/UsersApiTests.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.users; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static org.assertj.core.api.Assertions.assertThat; class UsersApiTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private UsersApi underTest; @BeforeEach void setUp() { // default with SonarCloud organization to trigger api base URL and isSonarCloud = true ServerApiHelper helper = mockServer.serverApiHelper("orgKey"); underTest = new UsersApi(helper); } @Test void should_return_user_id_on_sonarcloud() { mockServer.addStringResponse("/api/users/current", """ { "isLoggedIn": true, "id": "16c9b3b3-3f7e-4d61-91fe-31d731456c08", "login": "obiwan.kenobi" }"""); var id = underTest.getCurrentUserId(new SonarLintCancelMonitor()); assertThat(id).isEqualTo("16c9b3b3-3f7e-4d61-91fe-31d731456c08"); } @Test void should_return_user_id_on_sonarqube_server() { var helperSqs = mockServer.serverApiHelper(null); // isSonarCloud = false var api = new UsersApi(helperSqs); mockServer.addStringResponse("/api/users/current", """ { "isLoggedIn": true, "id": "00000000-0000-0000-0000-000000000001", "login": "obiwan.kenobi" }"""); var id = api.getCurrentUserId(new SonarLintCancelMonitor()); assertThat(id).isEqualTo("00000000-0000-0000-0000-000000000001"); } @Test void should_return_null_on_malformed_response() { mockServer.addStringResponse("/api/users/current", "{}"); var id = underTest.getCurrentUserId(new SonarLintCancelMonitor()); assertThat(id).isNull(); } } ================================================ FILE: backend/server-api/src/test/java/org/sonarsource/sonarlint/core/serverapi/util/ProtobufUtilTest.java ================================================ /* * SonarLint Core - Server API * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverapi.util; import com.google.protobuf.Message; import com.google.protobuf.Parser; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.sonarsource.sonarlint.core.serverapi.util.ProtobufUtil.readMessages; public class ProtobufUtilTest { private static final Common.Paging SOME_MESSAGE = Common.Paging.newBuilder().build(); private static final Parser SOME_PARSER = Common.Paging.parser(); @Test void test_readMessages_empty() throws IOException { try (InputStream inputStream = newEmptyStream()) { assertThat(readMessages(inputStream, SOME_PARSER)).isEmpty(); } } @Test void test_readMessages_multiple() throws IOException { var paging1 = SOME_MESSAGE; var paging2 = SOME_MESSAGE; try (InputStream inputStream = new ByteArrayInputStream(toByteArray(paging1, paging2))) { assertThat(readMessages(inputStream, paging1.getParserForType())).containsOnly(paging1, paging2); } } @Test void test_readMessages_error() { InputStream inputStream = new ByteArrayInputStream("trash".getBytes(StandardCharsets.UTF_8)); var thrown = assertThrows(IllegalStateException.class, () -> readMessages(inputStream, SOME_PARSER)); assertThat(thrown).hasMessage("failed to parse protobuf message"); } @Test void test_writeMessage_error() throws IOException { var out = mock(OutputStream.class); doThrow(IOException.class).when(out).write(any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()); var thrown = assertThrows(IllegalStateException.class, () -> ProtobufUtil.writeMessage(out, SOME_MESSAGE)); assertThat(thrown).hasMessageStartingWith("failed to write message"); } public static byte[] toByteArray(Message... messages) throws IOException { try (var byteStream = new ByteArrayOutputStream()) { for (Message msg : messages) { msg.writeDelimitedTo(byteStream); } return byteStream.toByteArray(); } } public static ByteArrayInputStream newEmptyStream() { return new ByteArrayInputStream(new byte[0]); } } ================================================ FILE: backend/server-api/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/server-api/src/test/resources/update/component_tree.pb ================================================  $8b745480-b598-4e34-af4a-cb2de1808e505org.sonarsource.sonarlint.intellij:sonarlint-intellij2SonarLint for IntelliJ IDEA:SonarLint for IntelliJ IDEABTRKbdefault-organizationr sonarlintzpublic" AViSPajkqePDAZzqZ8n1vorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.java2AbstractIssuesPanel.javaBFILJ@src/main/java/org/sonarlint/intellij/ui/AbstractIssuesPanel.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmMuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/AbstractNode.java2AbstractNode.javaBFILJ?src/main/java/org/sonarlint/intellij/ui/nodes/AbstractNode.javaRjavabdefault-organization" AVGSAZk6ShtJBwDkIzI7{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/AbstractSonarAction.java2AbstractSonarAction.javaBFILJEsrc/main/java/org/sonarlint/intellij/actions/AbstractSonarAction.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJKorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/AccumulatorIssueListener.java2AccumulatorIssueListener.javaBFILJIsrc/main/java/org/sonarlint/intellij/editor/AccumulatorIssueListener.javaRjavabdefault-organization" AWChh-u4_Q-0dz4JAcNlorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/AddEditExclusionDialog.java2AddEditExclusionDialog.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/project/AddEditExclusionDialog.javaRjavabdefault-organization" AVjz54nG7h1V61hFWcxMyorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/AnalysisCallback.java2AnalysisCallback.javaBFILJCsrc/main/java/org/sonarlint/intellij/analysis/AnalysisCallback.javaRjavabdefault-organization" AVYIST4At19XVl6V76Ct}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/AnalysisConfigurator.java2AnalysisConfigurator.javaBFILJGsrc/main/java/org/sonarlint/intellij/analysis/AnalysisConfigurator.javaRjavabdefault-organization" AWJEZZkOlHOQQAsrvh5Lzorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/AnalysisResultIssues.java2AnalysisResultIssues.javaBFILJDsrc/main/java/org/sonarlint/intellij/issue/AnalysisResultIssues.javaRjavabdefault-organization" AWJEZZkPlHOQQAsrvh5Ororg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/AnalysisResults.java2AnalysisResults.javaBFILJsrc/main/java/org/sonarlint/intellij/editor/EscapeHandler.javaRjavabdefault-organization" AWChh-u3_Q-0dz4JAcNcyorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/ExcludeFileAction.java2ExcludeFileAction.javaBFILJCsrc/main/java/org/sonarlint/intellij/actions/ExcludeFileAction.javaRjavabdefault-organization" AWChh-u4_Q-0dz4JAcNm|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/ExclusionItem.java2ExclusionItem.javaBFILJFsrc/main/java/org/sonarlint/intellij/config/project/ExclusionItem.javaRjavabdefault-organization" AWChh-u4_Q-0dz4JAcNn}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/ExclusionTable.java2ExclusionTable.javaBFILJGsrc/main/java/org/sonarlint/intellij/config/project/ExclusionTable.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmNqorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/FileNode.java2 FileNode.javaBFILJ;src/main/java/org/sonarlint/intellij/ui/nodes/FileNode.javaRjavabdefault-organization" AVlBCACVDdGu0TUEM4_Uqorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/FlowsTree.java2FlowsTree.javaBFILJ;src/main/java/org/sonarlint/intellij/ui/tree/FlowsTree.javaRjavabdefault-organization" AVlBCACVDdGu0TUEM4_V}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/FlowsTreeModelBuilder.java2FlowsTreeModelBuilder.javaBFILJGsrc/main/java/org/sonarlint/intellij/ui/tree/FlowsTreeModelBuilder.javaRjavabdefault-organization" AWOSUxTp_oWprfWtnuSnyorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/tasks/GetOrganizationTask.java2GetOrganizationTask.javaBFILJCsrc/main/java/org/sonarlint/intellij/tasks/GetOrganizationTask.javaRjavabdefault-organization" AVPR7c4C9txRcDXxXQ9korg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/GlobalConfigurationListener.java2 GlobalConfigurationListener.javaBFILJNsrc/main/java/org/sonarlint/intellij/messages/GlobalConfigurationListener.javaRjavabdefault-organization" AWChh-u3_Q-0dz4JAcNkorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/GlobalExclusionsPanel.java2GlobalExclusionsPanel.javaBFILJMsrc/main/java/org/sonarlint/intellij/config/global/GlobalExclusionsPanel.javaRjavabdefault-organization" AVPR7c4C9txRcDXxXQ9ltorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/GlobalLogOutput.java2GlobalLogOutput.javaBFILJ>src/main/java/org/sonarlint/intellij/util/GlobalLogOutput.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg4org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/IndexedObjectStore.java2IndexedObjectStore.javaBFILJNsrc/main/java/org/sonarlint/intellij/issue/persistence/IndexedObjectStore.javaRjavabdefault-organization" AV1RVebIo6Qmn8dFiYtIzorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/tasks/InformationFetchTask.java2InformationFetchTask.javaBFILJDsrc/main/java/org/sonarlint/intellij/tasks/InformationFetchTask.javaRjavabdefault-organization" AVLvZ9LbSacnzMyR1mlutorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/tracking/Input.java2 Input.javaBFILJ>src/main/java/org/sonarlint/intellij/issue/tracking/Input.javaRjavabdefault-organization" AVjd6xIgFr_J5Dpp7BbDorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/exception/InvalidBindingException.java2InvalidBindingException.javaBFILJKsrc/main/java/org/sonarlint/intellij/exception/InvalidBindingException.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKgzrorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/IssueManager.java2IssueManager.javaBFILJsrc/main/java/org/sonarlint/intellij/issue/IssueProcessor.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJSporg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/IssueStore.java2IssueStore.javaBFILJ:src/main/java/org/sonarlint/intellij/issue/IssueStore.javaRjavabdefault-organization" AVRxXmKqeOKlMxH0tQUt{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/IssueStoreListener.java2IssueStoreListener.javaBFILJEsrc/main/java/org/sonarlint/intellij/messages/IssueStoreListener.javaRjavabdefault-organization" AVo9epi9R1_D6kvz7Yax{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/IssuesViewTabOpener.java2IssuesViewTabOpener.javaBFILJEsrc/main/java/org/sonarlint/intellij/actions/IssuesViewTabOpener.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmYqorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/IssueTree.java2IssueTree.javaBFILJ;src/main/java/org/sonarlint/intellij/ui/tree/IssueTree.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmavorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/IssueTreeIndex.java2IssueTreeIndex.javaBFILJ@src/main/java/org/sonarlint/intellij/ui/tree/IssueTreeIndex.javaRjavabdefault-organization" AVlBCACWDdGu0TUEM4_W}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/IssueTreeModelBuilder.java2IssueTreeModelBuilder.javaBFILJGsrc/main/java/org/sonarlint/intellij/ui/tree/IssueTreeModelBuilder.javaRjavabdefault-organization" AVYIST4At19XVl6V76Cuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/JavaAnalysisConfigurator.java2JavaAnalysisConfigurator.javaBFILJKsrc/main/java/org/sonarlint/intellij/analysis/JavaAnalysisConfigurator.javaRjavabdefault-organization" AVlBCACUDdGu0TUEM4_Srorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/LabelNode.java2LabelNode.javaBFILJsrc/main/java/org/sonarlint/intellij/ui/LastAnalysisPanel.javaRjavabdefault-organization" AVzAhrSdRLymfFZDbbDKorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/wizard/LengthRestrictedDocument.java2LengthRestrictedDocument.javaBFILJWsrc/main/java/org/sonarlint/intellij/config/global/wizard/LengthRestrictedDocument.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg0oorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/LiveIssue.java2LiveIssue.javaBFILJ9src/main/java/org/sonarlint/intellij/issue/LiveIssue.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg6org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/LiveIssueCache.java2LiveIssueCache.javaBFILJJsrc/main/java/org/sonarlint/intellij/issue/persistence/LiveIssueCache.javaRjavabdefault-organization" AWChh-u3_Q-0dz4JAcNe|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/LocalFileExclusions.java2LocalFileExclusions.javaBFILJFsrc/main/java/org/sonarlint/intellij/analysis/LocalFileExclusions.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg1yorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/LocalIssueTrackable.java2LocalIssueTrackable.javaBFILJCsrc/main/java/org/sonarlint/intellij/issue/LocalIssueTrackable.javaRjavabdefault-organization" AVlBCACUDdGu0TUEM4_Tuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/LocationNode.java2LocationNode.javaBFILJ?src/main/java/org/sonarlint/intellij/ui/nodes/LocationNode.javaRjavabdefault-organization" AVRxtLJXeOKlMxH0tR9Hsorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/trigger/MakeTrigger.java2MakeTrigger.javaBFILJ=src/main/java/org/sonarlint/intellij/trigger/MakeTrigger.javaRjavabdefault-organization" AVzAhrSdRLymfFZDbbDLorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/wizard/OrganizationStep.java2OrganizationStep.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/global/wizard/OrganizationStep.javaRjavabdefault-organization" AWChh-u3_Q-0dz4JAcNj}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/component/package-info.java2package-info.javaBFILJGsrc/main/java/org/sonarlint/intellij/config/component/package-info.javaRjavabdefault-organization" AVzAhrSdRLymfFZDbbDPorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/wizard/package-info.java2package-info.javaBFILJKsrc/main/java/org/sonarlint/intellij/config/global/wizard/package-info.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAeQorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/package-info.java2package-info.javaBFILJJsrc/main/java/org/sonarlint/intellij/config/global/rules/package-info.javaRjavabdefault-organization" AVoeoD-jXQg2W5SJWYi4rorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/tasks/package-info.java2package-info.javaBFILJsrc/main/java/org/sonarlint/intellij/actions/package-info.javaRjavabdefault-organization" AVGSAZk6ShtJBwDkIzI5lorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/package-info.java2package-info.javaBFILJ6src/main/java/org/sonarlint/intellij/package-info.javaRjavabdefault-organization" AVGSAZk7ShtJBwDkIzJIuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/package-info.java2package-info.javaBFILJ?src/main/java/org/sonarlint/intellij/analysis/package-info.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJOsorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/package-info.java2package-info.javaBFILJ=src/main/java/org/sonarlint/intellij/editor/package-info.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJTrorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/package-info.java2package-info.javaBFILJsrc/main/java/org/sonarlint/intellij/trigger/package-info.javaRjavabdefault-organization" AVGSAZk_ShtJBwDkIzJeoorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/package-info.java2package-info.javaBFILJ9src/main/java/org/sonarlint/intellij/ui/package-info.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyNBzorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/package-info.java2package-info.javaBFILJDsrc/main/java/org/sonarlint/intellij/config/global/package-info.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyNG{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/package-info.java2package-info.javaBFILJEsrc/main/java/org/sonarlint/intellij/config/project/package-info.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmRuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/package-info.java2package-info.javaBFILJ?src/main/java/org/sonarlint/intellij/ui/nodes/package-info.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmctorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/package-info.java2package-info.javaBFILJ>src/main/java/org/sonarlint/intellij/ui/tree/package-info.javaRjavabdefault-organization" AVJAh5MfnavCjZLaBRPkuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/package-info.java2package-info.javaBFILJ?src/main/java/org/sonarlint/intellij/messages/package-info.javaRjavabdefault-organization" AVLvZ9LbSacnzMyR1mly{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/tracking/package-info.java2package-info.javaBFILJEsrc/main/java/org/sonarlint/intellij/issue/tracking/package-info.javaRjavabdefault-organization" AVPR7c4C9txRcDXxXQ9iqorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/package-info.java2package-info.javaBFILJ;src/main/java/org/sonarlint/intellij/core/package-info.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg_~org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/package-info.java2package-info.javaBFILJHsrc/main/java/org/sonarlint/intellij/issue/persistence/package-info.javaRjavabdefault-organization" AVfdcixoJ96w3vssdN0jorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/PathStoreKeyValidator.java2PathStoreKeyValidator.javaBFILJQsrc/main/java/org/sonarlint/intellij/issue/persistence/PathStoreKeyValidator.javaRjavabdefault-organization" AVfdcixoJ96w3vssdN0gzorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/ProjectBindingManager.java2ProjectBindingManager.javaBFILJDsrc/main/java/org/sonarlint/intellij/core/ProjectBindingManager.javaRjavabdefault-organization" AV1RVebHo6Qmn8dFiYtHorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/ProjectConfigurationListener.java2!ProjectConfigurationListener.javaBFILJOsrc/main/java/org/sonarlint/intellij/messages/ProjectConfigurationListener.javaRjavabdefault-organization" AWChh-u4_Q-0dz4JAcNoorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/ProjectExclusionsPanel.java2ProjectExclusionsPanel.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/project/ProjectExclusionsPanel.javaRjavabdefault-organization" AVPR7c4C9txRcDXxXQ9muorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/ProjectLogOutput.java2ProjectLogOutput.javaBFILJ?src/main/java/org/sonarlint/intellij/util/ProjectLogOutput.javaRjavabdefault-organization" AVlBCACNDdGu0TUEM4_Psorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/RangeBlinker.java2RangeBlinker.javaBFILJ=src/main/java/org/sonarlint/intellij/editor/RangeBlinker.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAeLorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.java2RuleConfigurationPanel.javaBFILJTsrc/main/java/org/sonarlint/intellij/config/global/rules/RuleConfigurationPanel.javaRjavabdefault-organization" AWSDLlRRl658ZWmGCyVForg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesFilterAction.java2RulesFilterAction.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesFilterAction.javaRjavabdefault-organization" AWSDLlRRl658ZWmGCyVGorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesFilterModel.java2RulesFilterModel.javaBFILJNsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesFilterModel.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAeMorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.java2RulesTreeNode.javaBFILJKsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeNode.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAeNorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTable.java2RulesTreeTable.javaBFILJLsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTable.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAeOorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModel.java2RulesTreeTableModel.javaBFILJQsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableModel.javaRjavabdefault-organization" AWR-SuIci6PwnvJ9RAePorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableRenderer.java2RulesTreeTableRenderer.javaBFILJTsrc/main/java/org/sonarlint/intellij/config/global/rules/RulesTreeTableRenderer.javaRjavabdefault-organization" AWJTMlNaorNz2_dLi6RRorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SearchProjectKeyDialog.java2SearchProjectKeyDialog.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/project/SearchProjectKeyDialog.javaRjavabdefault-organization" AVoeoD-jXQg2W5SJWYi3org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/tasks/ServerDownloadProjectTask.java2ServerDownloadProjectTask.javaBFILJIsrc/main/java/org/sonarlint/intellij/tasks/ServerDownloadProjectTask.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg2zorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/ServerIssueTrackable.java2ServerIssueTrackable.javaBFILJDsrc/main/java/org/sonarlint/intellij/issue/ServerIssueTrackable.javaRjavabdefault-organization" AVfTIlxMwczdZ2UaLhntworg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/ServerIssueUpdater.java2ServerIssueUpdater.javaBFILJAsrc/main/java/org/sonarlint/intellij/core/ServerIssueUpdater.javaRjavabdefault-organization" AVzAhrSdRLymfFZDbbDNorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/wizard/ServerStep.java2ServerStep.javaBFILJIsrc/main/java/org/sonarlint/intellij/config/global/wizard/ServerStep.javaRjavabdefault-organization" AVoeoD-jXQg2W5SJWYi5vorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/tasks/ServerUpdateTask.java2ServerUpdateTask.javaBFILJ@src/main/java/org/sonarlint/intellij/tasks/ServerUpdateTask.javaRjavabdefault-organization" AWJEZZkNlHOQQAsrvh5Gorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/ShowAnalysisResultsCallable.java2 ShowAnalysisResultsCallable.javaBFILJMsrc/main/java/org/sonarlint/intellij/actions/ShowAnalysisResultsCallable.javaRjavabdefault-organization" AWJTMlNYorNz2_dLi6RQorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/ShowCurrentFileCallable.java2ShowCurrentFileCallable.javaBFILJIsrc/main/java/org/sonarlint/intellij/actions/ShowCurrentFileCallable.javaRjavabdefault-organization" AVmIAJwxG7g4kk4HeclZ}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/ShowLocationsIntention.java2ShowLocationsIntention.javaBFILJGsrc/main/java/org/sonarlint/intellij/editor/ShowLocationsIntention.javaRjavabdefault-organization" AVoZFzCFh-gplmkNkOeJorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarAnalyzeAllFilesAction.java2SonarAnalyzeAllFilesAction.javaBFILJLsrc/main/java/org/sonarlint/intellij/actions/SonarAnalyzeAllFilesAction.javaRjavabdefault-organization" AViM6p0NcMUueO7VmA_Worg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarAnalyzeChangedFilesAction.java2#SonarAnalyzeChangedFilesAction.javaBFILJPsrc/main/java/org/sonarlint/intellij/actions/SonarAnalyzeChangedFilesAction.javaRjavabdefault-organization" AWChh-u3_Q-0dz4JAcNdorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarAnalyzeFilesAction.java2SonarAnalyzeFilesAction.javaBFILJIsrc/main/java/org/sonarlint/intellij/actions/SonarAnalyzeFilesAction.javaRjavabdefault-organization" AVGSAZk5ShtJBwDkIzI4porg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/SonarApplication.java2SonarApplication.javaBFILJ:src/main/java/org/sonarlint/intellij/SonarApplication.javaRjavabdefault-organization" AWJEZZkNlHOQQAsrvh5Hyorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarCancelAction.java2SonarCancelAction.javaBFILJCsrc/main/java/org/sonarlint/intellij/actions/SonarCancelAction.javaRjavabdefault-organization" AWJEZZkNlHOQQAsrvh5Iorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarCleanConsoleAction.java2SonarCleanConsoleAction.javaBFILJIsrc/main/java/org/sonarlint/intellij/actions/SonarCleanConsoleAction.javaRjavabdefault-organization" AWJEZZkNlHOQQAsrvh5Jorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarClearAnalysisResultsAction.java2$SonarClearAnalysisResultsAction.javaBFILJQsrc/main/java/org/sonarlint/intellij/actions/SonarClearAnalysisResultsAction.javaRjavabdefault-organization" AWJEZZkNlHOQQAsrvh5K~org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarClearIssuesAction.java2SonarClearIssuesAction.javaBFILJHsrc/main/java/org/sonarlint/intellij/actions/SonarClearIssuesAction.javaRjavabdefault-organization" AVGSAZk6ShtJBwDkIzI_}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarConfigureProject.java2SonarConfigureProject.javaBFILJGsrc/main/java/org/sonarlint/intellij/actions/SonarConfigureProject.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJL}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/SonarExternalAnnotator.java2SonarExternalAnnotator.javaBFILJGsrc/main/java/org/sonarlint/intellij/editor/SonarExternalAnnotator.javaRjavabdefault-organization" AVGSAZk8ShtJBwDkIzJNworg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/SonarLinkHandler.java2SonarLinkHandler.javaBFILJAsrc/main/java/org/sonarlint/intellij/editor/SonarLinkHandler.javaRjavabdefault-organization" AVrWXSHCXDyvoKp0u12Forg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarLintAboutPanel.java2SonarLintAboutPanel.javaBFILJKsrc/main/java/org/sonarlint/intellij/config/global/SonarLintAboutPanel.javaRjavabdefault-organization" AVpMbFbfVFbuW6S1_-dyuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/SonarLintActions.java2SonarLintActions.javaBFILJ?src/main/java/org/sonarlint/intellij/util/SonarLintActions.javaRjavabdefault-organization" AVpHA0JpG3VlJy9H2ntkorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.java2"SonarLintAnalysisResultsPanel.javaBFILJJsrc/main/java/org/sonarlint/intellij/ui/SonarLintAnalysisResultsPanel.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyM7zorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintAnalyzer.java2SonarLintAnalyzer.javaBFILJDsrc/main/java/org/sonarlint/intellij/analysis/SonarLintAnalyzer.javaRjavabdefault-organization" AVf2Vn5xidHM9RGl4f66vorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/SonarLintAppUtils.java2SonarLintAppUtils.javaBFILJ@src/main/java/org/sonarlint/intellij/util/SonarLintAppUtils.javaRjavabdefault-organization" AVBBpXODintm2-s7WOQ8torg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/SonarLintBundle.java2SonarLintBundle.javaBFILJ>src/main/java/org/sonarlint/intellij/util/SonarLintBundle.javaRjavabdefault-organization" AViM6p0PcMUueO7VmA_corg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/trigger/SonarLintCheckinHandler.java2SonarLintCheckinHandler.javaBFILJIsrc/main/java/org/sonarlint/intellij/trigger/SonarLintCheckinHandler.javaRjavabdefault-organization" AViM6p0PcMUueO7VmA_dorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/trigger/SonarLintCheckinHandlerFactory.java2#SonarLintCheckinHandlerFactory.javaBFILJPsrc/main/java/org/sonarlint/intellij/trigger/SonarLintCheckinHandlerFactory.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyM8org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/SonarLintColorSettingsPage.java2SonarLintColorSettingsPage.javaBFILJKsrc/main/java/org/sonarlint/intellij/config/SonarLintColorSettingsPage.javaRjavabdefault-organization" AVGSAZk9ShtJBwDkIzJbsorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintConsole.java2SonarLintConsole.javaBFILJ=src/main/java/org/sonarlint/intellij/ui/SonarLintConsole.javaRjavabdefault-organization" AVfdcixoJ96w3vssdN0h{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/SonarLintEngineFactory.java2SonarLintEngineFactory.javaBFILJEsrc/main/java/org/sonarlint/intellij/core/SonarLintEngineFactory.javaRjavabdefault-organization" AVfdcixoJ96w3vssdN0i{org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/SonarLintEngineManager.java2SonarLintEngineManager.javaBFILJEsrc/main/java/org/sonarlint/intellij/core/SonarLintEngineManager.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9etorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/SonarLintFacade.java2SonarLintFacade.javaBFILJ>src/main/java/org/sonarlint/intellij/core/SonarLintFacade.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyM-org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalConfigurable.java2 SonarLintGlobalConfigurable.javaBFILJSsrc/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalConfigurable.javaRjavabdefault-organization" AVrWXSHCXDyvoKp0u12Gorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.java2 SonarLintGlobalOptionsPanel.javaBFILJSsrc/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalOptionsPanel.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyM_org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalSettings.java2SonarLintGlobalSettings.javaBFILJOsrc/main/java/org/sonarlint/intellij/config/global/SonarLintGlobalSettings.javaRjavabdefault-organization" AVlBCACNDdGu0TUEM4_Q|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/editor/SonarLintHighlighting.java2SonarLintHighlighting.javaBFILJFsrc/main/java/org/sonarlint/intellij/editor/SonarLintHighlighting.javaRjavabdefault-organization" AVjjZHb_jW8OMIIyRCJ_]org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/icons/SonarLintIcons.java2SonarLintIcons.javaBFILJ'src/main/java/icons/SonarLintIcons.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmLworg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.java2SonarLintIssuesPanel.javaBFILJAsrc/main/java/org/sonarlint/intellij/ui/SonarLintIssuesPanel.javaRjavabdefault-organization" AVfr4xyZl8r7s5DFVXXduorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintJob.java2SonarLintJob.javaBFILJ?src/main/java/org/sonarlint/intellij/analysis/SonarLintJob.javaRjavabdefault-organization" AVYIST4At19XVl6V76Cv|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintJobManager.java2SonarLintJobManager.javaBFILJFsrc/main/java/org/sonarlint/intellij/analysis/SonarLintJobManager.javaRjavabdefault-organization" AVRxtLJYeOKlMxH0tR9Itorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintLogPanel.java2SonarLintLogPanel.javaBFILJ>src/main/java/org/sonarlint/intellij/ui/SonarLintLogPanel.javaRjavabdefault-organization" AWEswAfEr8h_fVHZ4UsWorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintProjectAnalyzersPanel.java2#SonarLintProjectAnalyzersPanel.javaBFILJKsrc/main/java/org/sonarlint/intellij/ui/SonarLintProjectAnalyzersPanel.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9Zorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectBindPanel.java2SonarLintProjectBindPanel.javaBFILJRsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectBindPanel.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyNDorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectConfigurable.java2!SonarLintProjectConfigurable.javaBFILJUsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectConfigurable.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9forg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/SonarLintProjectNotifications.java2"SonarLintProjectNotifications.javaBFILJLsrc/main/java/org/sonarlint/intellij/core/SonarLintProjectNotifications.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9aorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectPropertiesPanel.java2$SonarLintProjectPropertiesPanel.javaBFILJXsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectPropertiesPanel.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyNEorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectSettings.java2SonarLintProjectSettings.javaBFILJQsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectSettings.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyNForg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectSettingsPanel.java2"SonarLintProjectSettingsPanel.javaBFILJVsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectSettingsPanel.javaRjavabdefault-organization" AV1RVebXo6Qmn8dFiYtKorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/project/SonarLintProjectState.java2SonarLintProjectState.javaBFILJNsrc/main/java/org/sonarlint/intellij/config/project/SonarLintProjectState.javaRjavabdefault-organization" AVKsh-RkwW7gmwRPbWJ_uorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.java2SonarLintRulePanel.javaBFILJ?src/main/java/org/sonarlint/intellij/ui/SonarLintRulePanel.javaRjavabdefault-organization" AVI_VGQpMaJn9Xn8TGmevorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/SonarLintSeverity.java2SonarLintSeverity.javaBFILJ@src/main/java/org/sonarlint/intellij/util/SonarLintSeverity.javaRjavabdefault-organization" AVGSAZk7ShtJBwDkIzJExorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintStatus.java2SonarLintStatus.javaBFILJBsrc/main/java/org/sonarlint/intellij/analysis/SonarLintStatus.javaRjavabdefault-organization" AViM6p0PcMUueO7VmA_ezorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/trigger/SonarLintSubmitter.java2SonarLintSubmitter.javaBFILJDsrc/main/java/org/sonarlint/intellij/trigger/SonarLintSubmitter.javaRjavabdefault-organization" AVGSAZk7ShtJBwDkIzJFvorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintTask.java2SonarLintTask.javaBFILJ@src/main/java/org/sonarlint/intellij/analysis/SonarLintTask.javaRjavabdefault-organization" AVjz0PKA7h1V61hFWcCu}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintTaskFactory.java2SonarLintTaskFactory.javaBFILJGsrc/main/java/org/sonarlint/intellij/analysis/SonarLintTaskFactory.javaRjavabdefault-organization" AVqo6rkFmafQcz5ZZbCo|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/telemetry/SonarLintTelemetry.java2SonarLintTelemetry.javaBFILJFsrc/main/java/org/sonarlint/intellij/telemetry/SonarLintTelemetry.javaRjavabdefault-organization" AVI7z0IbaQmCg8KqUyM9~org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/SonarLintTextAttributes.java2SonarLintTextAttributes.javaBFILJHsrc/main/java/org/sonarlint/intellij/config/SonarLintTextAttributes.javaRjavabdefault-organization" AVGSAZk_ShtJBwDkIzJd}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.java2SonarLintToolWindowFactory.javaBFILJGsrc/main/java/org/sonarlint/intellij/ui/SonarLintToolWindowFactory.javaRjavabdefault-organization" AViM6p0NcMUueO7VmA_Zzorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/analysis/SonarLintUserTask.java2SonarLintUserTask.javaBFILJDsrc/main/java/org/sonarlint/intellij/analysis/SonarLintUserTask.javaRjavabdefault-organization" AVGSAZlAShtJBwDkIzJhsorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/SonarLintUtils.java2SonarLintUtils.javaBFILJ=src/main/java/org/sonarlint/intellij/util/SonarLintUtils.javaRjavabdefault-organization" AV1RVebOo6Qmn8dFiYtJorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/SonarQubeEventNotifications.java2 SonarQubeEventNotifications.javaBFILJJsrc/main/java/org/sonarlint/intellij/core/SonarQubeEventNotifications.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9V}org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarQubeServer.java2SonarQubeServer.javaBFILJGsrc/main/java/org/sonarlint/intellij/config/global/SonarQubeServer.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9Xorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/SonarQubeServerMgmtPanel.java2SonarQubeServerMgmtPanel.javaBFILJPsrc/main/java/org/sonarlint/intellij/config/global/SonarQubeServerMgmtPanel.javaRjavabdefault-organization" AWEswAfBr8h_fVHZ4UsU~org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/SonarShowCodeAnalyzers.java2SonarShowCodeAnalyzers.javaBFILJHsrc/main/java/org/sonarlint/intellij/actions/SonarShowCodeAnalyzers.javaRjavabdefault-organization" AVzAhrSdRLymfFZDbbDMorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/config/global/wizard/SQServerWizard.java2SQServerWizard.javaBFILJMsrc/main/java/org/sonarlint/intellij/config/global/wizard/SQServerWizard.javaRjavabdefault-organization" AVPR7c4B9txRcDXxXQ9h~org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/StandaloneSonarLintFacade.java2StandaloneSonarLintFacade.javaBFILJHsrc/main/java/org/sonarlint/intellij/core/StandaloneSonarLintFacade.javaRjavabdefault-organization" AVI7z0IcaQmCg8KqUyNIworg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/StatusListener.java2StatusListener.javaBFILJAsrc/main/java/org/sonarlint/intellij/messages/StatusListener.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg8|org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/StoreIndex.java2StoreIndex.javaBFILJFsrc/main/java/org/sonarlint/intellij/issue/persistence/StoreIndex.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg9org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/StoreKeyValidator.java2StoreKeyValidator.javaBFILJMsrc/main/java/org/sonarlint/intellij/issue/persistence/StoreKeyValidator.javaRjavabdefault-organization" AVfcmCn7J96w3vssdKg-org.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/persistence/StringStoreIndex.java2StringStoreIndex.javaBFILJLsrc/main/java/org/sonarlint/intellij/issue/persistence/StringStoreIndex.javaRjavabdefault-organization" AVI_VGQoMaJn9Xn8TGmPtorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/nodes/SummaryNode.java2SummaryNode.javaBFILJ>src/main/java/org/sonarlint/intellij/ui/nodes/SummaryNode.javaRjavabdefault-organization" AVI7z0IcaQmCg8KqUyNJuorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/messages/TaskListener.java2TaskListener.javaBFILJ?src/main/java/org/sonarlint/intellij/messages/TaskListener.javaRjavabdefault-organization" AVP2V6LzJSJ5Fb--AuHKxorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/util/TaskProgressMonitor.java2TaskProgressMonitor.javaBFILJBsrc/main/java/org/sonarlint/intellij/util/TaskProgressMonitor.javaRjavabdefault-organization" AVrWXSHDXDyvoKp0u12Horg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/telemetry/TelemetryEngineProvider.java2TelemetryEngineProvider.javaBFILJKsrc/main/java/org/sonarlint/intellij/telemetry/TelemetryEngineProvider.javaRjavabdefault-organization" AVP2V6LyJSJ5Fb--AuHHorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/ToolWindowLogAnalysisAction.java2 ToolWindowLogAnalysisAction.javaBFILJMsrc/main/java/org/sonarlint/intellij/actions/ToolWindowLogAnalysisAction.javaRjavabdefault-organization" AVGSAZk6ShtJBwDkIzJBorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/actions/ToolWindowVerboseModeAction.java2 ToolWindowVerboseModeAction.javaBFILJMsrc/main/java/org/sonarlint/intellij/actions/ToolWindowVerboseModeAction.javaRjavabdefault-organization" AVLvZ9LbSacnzMyR1mlvxorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/tracking/Trackable.java2Trackable.javaBFILJBsrc/main/java/org/sonarlint/intellij/issue/tracking/Trackable.javaRjavabdefault-organization" AVLvZ9LbSacnzMyR1mlwvorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/tracking/Tracker.java2 Tracker.javaBFILJ@src/main/java/org/sonarlint/intellij/issue/tracking/Tracker.javaRjavabdefault-organization" AVLvZ9LbSacnzMyR1mlxworg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/issue/tracking/Tracking.java2 Tracking.javaBFILJAsrc/main/java/org/sonarlint/intellij/issue/tracking/Tracking.javaRjavabdefault-organization" AVlPHdNrJfqetNCTTETWxorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/ui/tree/TreeCellRenderer.java2TreeCellRenderer.javaBFILJBsrc/main/java/org/sonarlint/intellij/ui/tree/TreeCellRenderer.javaRjavabdefault-organization" AVYDL9N7YM5w3R5Wcr8csorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/trigger/TriggerType.java2TriggerType.javaBFILJ=src/main/java/org/sonarlint/intellij/trigger/TriggerType.javaRjavabdefault-organization" AViNHYcmcMUueO7VmCdvrorg.sonarsource.sonarlint.intellij:sonarlint-intellij:src/main/java/org/sonarlint/intellij/core/UpdateChecker.java2UpdateChecker.javaBFILJ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-server-connection SonarLint Core - Server Connection Manage connections with SonarQube or SonarCloud 2.0.1 com.google.code.findbugs jsr305 provided org.apache.commons commons-lang3 commons-io commons-io org.apache.commons commons-compress commons-codec commons-codec ${project.groupId} sonarlint-commons ${project.version} ${project.groupId} sonarlint-server-api ${project.version} com.google.protobuf protobuf-java ${protobuf.version} org.jetbrains.xodus xodus-entity-store ${xodus.version} org.slf4j slf4j-api org.jetbrains.xodus xodus-environment ${xodus.version} org.slf4j slf4j-api org.jetbrains.xodus xodus-vfs ${xodus.version} com.squareup.okhttp3 okhttp test org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test com.squareup.okhttp3 mockwebserver3 test org.junit.jupiter junit-jupiter-params test ch.qos.logback logback-classic test kr.motd.maven os-maven-plugin initialize detect org.xolstice.maven.plugins protobuf-maven-plugin compile test-compile com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/AnalyzerConfiguration.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Map; public class AnalyzerConfiguration { public static final int CURRENT_SCHEMA_VERSION = 1; private final Settings settings; private final Map ruleSetByLanguageKey; private final int schemaVersion; public AnalyzerConfiguration(Settings settings, Map ruleSetByLanguageKey, int schemaVersion) { this.settings = settings; this.ruleSetByLanguageKey = ruleSetByLanguageKey; this.schemaVersion = schemaVersion; } public Settings getSettings() { return settings; } public Map getRuleSetByLanguageKey() { return ruleSetByLanguageKey; } public int getSchemaVersion() { return schemaVersion; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/AnalyzerConfigurationStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.push.parsing.common.ImpactPayload; import org.sonarsource.sonarlint.core.serverapi.rules.ServerActiveRule; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil; import org.sonarsource.sonarlint.core.serverconnection.storage.RWLock; import org.sonarsource.sonarlint.core.serverconnection.storage.StorageException; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class AnalyzerConfigurationStorage { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final RWLock rwLock = new RWLock(); private final Path storageFilePath; public AnalyzerConfigurationStorage(Path projectStorageRoot) { this.storageFilePath = projectStorageRoot.resolve("analyzer_config.pb"); } public boolean isValid() { if (!Files.exists(storageFilePath)) { LOG.debug("Analyzer configuration storage doesn't exist: {}", storageFilePath); return false; } return tryRead().isPresent(); } public void store(AnalyzerConfiguration analyzerConfiguration) { FileUtils.mkdirs(storageFilePath.getParent()); var data = adapt(analyzerConfiguration); LOG.debug("Storing project analyzer configuration in {}", storageFilePath); rwLock.write(() -> writeToFile(data, storageFilePath)); LOG.debug("Stored project analyzer configuration"); } private Optional tryRead() { try { return Optional.of(read()); } catch (Exception e) { LOG.debug("Could not load analyzer configuration storage", e); return Optional.empty(); } } public AnalyzerConfiguration read() { return adapt(rwLock.read(() -> readConfiguration(storageFilePath))); } public void update(UnaryOperator updater) { FileUtils.mkdirs(storageFilePath.getParent()); rwLock.write(() -> { Sonarlint.AnalyzerConfiguration config; try { config = readConfiguration(storageFilePath); } catch (StorageException e) { LOG.warn("Unable to read storage. Creating a new one.", e); config = Sonarlint.AnalyzerConfiguration.newBuilder().build(); } writeToFile(adapt(updater.apply(adapt(config))), storageFilePath); LOG.debug("Storing project data in {}", storageFilePath); }); } private static Sonarlint.AnalyzerConfiguration readConfiguration(Path projectFilePath) { return ProtobufFileUtil.readFile(projectFilePath, Sonarlint.AnalyzerConfiguration.parser()); } private static AnalyzerConfiguration adapt(Sonarlint.AnalyzerConfiguration analyzerConfiguration) { return new AnalyzerConfiguration( new Settings(analyzerConfiguration.getSettingsMap()), analyzerConfiguration.getRuleSetsByLanguageKeyMap().entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, e -> adapt(e.getValue()))), analyzerConfiguration.getSchemaVersion()); } private static Sonarlint.AnalyzerConfiguration adapt(AnalyzerConfiguration analyzerConfiguration) { return Sonarlint.AnalyzerConfiguration.newBuilder() .setSchemaVersion(analyzerConfiguration.getSchemaVersion()) .putAllSettings(analyzerConfiguration.getSettings().getAll()) .putAllRuleSetsByLanguageKey(analyzerConfiguration.getRuleSetByLanguageKey().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> adapt(e.getValue())))) .build(); } private static RuleSet adapt(Sonarlint.RuleSet ruleSet) { return new RuleSet( ruleSet.getRuleList().stream().map(AnalyzerConfigurationStorage::adapt).toList(), ruleSet.getLastModified()); } private static ServerActiveRule adapt(Sonarlint.RuleSet.ActiveRule rule) { return new ServerActiveRule( rule.getRuleKey(), IssueSeverity.valueOf(rule.getSeverity()), rule.getParamsMap(), rule.getTemplateKey(), rule.getOverriddenImpactsList().stream() .map(impact -> new ImpactPayload(impact.getSoftwareQuality(), impact.getSeverity())) .toList()); } private static Sonarlint.RuleSet adapt(RuleSet ruleSet) { return Sonarlint.RuleSet.newBuilder() .setLastModified(ruleSet.getLastModified()) .addAllRule(ruleSet.getRules().stream().map(AnalyzerConfigurationStorage::adapt).toList()).build(); } private static Sonarlint.RuleSet.ActiveRule adapt(ServerActiveRule rule) { return Sonarlint.RuleSet.ActiveRule.newBuilder() .setRuleKey(rule.getRuleKey()) .setSeverity(rule.getSeverity().name()) .setTemplateKey(rule.getTemplateKey()) .putAllParams(rule.getParams()) .addAllOverriddenImpacts(rule.getOverriddenImpacts().stream() .map(impact -> Sonarlint.RuleSet.ActiveRule.newBuilder().addOverriddenImpactsBuilder() .setSoftwareQuality(impact.getSoftwareQuality()) .setSeverity(impact.getSeverity()) .build()) .toList()) .build(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/AnalyzerSettingsUpdateSummary.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Map; public class AnalyzerSettingsUpdateSummary { private final Map updatedSettingsValueByKey; public AnalyzerSettingsUpdateSummary(Map updatedSettingsValueByKey) { this.updatedSettingsValueByKey = updatedSettingsValueByKey; } public Map getUpdatedSettingsValueByKey() { return updatedSettingsValueByKey; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ConnectionStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.serverconnection.storage.OrganizationStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueStoresManager; import org.sonarsource.sonarlint.core.serverconnection.storage.UserStorage; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs; public class ConnectionStorage { private final ServerIssueStoresManager serverIssueStoresManager; private final ServerInfoStorage serverInfoStorage; private final Map sonarProjectStorageByKey = new ConcurrentHashMap<>(); private final Path projectsStorageRoot; private final PluginsStorage pluginsStorage; private final Path connectionStorageRoot; private final OrganizationStorage organizationStorage; private final String connectionId; private final UserStorage userStorage; public ConnectionStorage(Path globalStorageRoot, String connectionId, SonarLintDatabase database) { this.connectionId = connectionId; this.connectionStorageRoot = globalStorageRoot.resolve(encodeForFs(connectionId)); this.projectsStorageRoot = connectionStorageRoot.resolve("projects"); this.serverIssueStoresManager = new ServerIssueStoresManager(connectionId, database); this.serverInfoStorage = new ServerInfoStorage(connectionStorageRoot); this.pluginsStorage = new PluginsStorage(connectionStorageRoot); this.organizationStorage = new OrganizationStorage(connectionStorageRoot); this.userStorage = new UserStorage(connectionStorageRoot); } public ServerInfoStorage serverInfo() { return serverInfoStorage; } public SonarProjectStorage project(String sonarProjectKey) { return sonarProjectStorageByKey.computeIfAbsent(sonarProjectKey, k -> new SonarProjectStorage(projectsStorageRoot, serverIssueStoresManager, sonarProjectKey)); } public PluginsStorage plugins() { return pluginsStorage; } public OrganizationStorage organization() { return organizationStorage; } public UserStorage user() { return userStorage; } public String connectionId() { return connectionId; } public void delete() { FileUtils.deleteRecursively(connectionStorageRoot); serverIssueStoresManager.delete(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/DownloadException.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.SonarLintException; public class DownloadException extends SonarLintException { public DownloadException() { super(); } public DownloadException(String msg, @Nullable Throwable cause) { super(msg, cause); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/DownloaderUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; public class DownloaderUtils { private DownloaderUtils() { // Utility class } public static SoftwareQuality parseProtoSoftwareQuality(Common.Impact protoImpact) { if (!protoImpact.hasSoftwareQuality() || protoImpact.getSoftwareQuality() == Common.SoftwareQuality.UNKNOWN_IMPACT_QUALITY) { throw new IllegalArgumentException("Unknown or missing software quality"); } return SoftwareQuality.valueOf(protoImpact.getSoftwareQuality().name()); } public static ImpactSeverity parseProtoImpactSeverity(Common.Impact protoImpact) { if (!protoImpact.hasSeverity() || protoImpact.getSeverity() == Common.ImpactSeverity.UNKNOWN_IMPACT_SEVERITY) { throw new IllegalArgumentException("Unknown or missing impact severity"); } return ImpactSeverity.mapSeverity(protoImpact.getSeverity().name()); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/FileUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.IOException; import java.nio.file.AccessDeniedException; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.function.Consumer; import org.apache.commons.io.file.PathUtils; public class FileUtils { /** * A simple representation of an IO operation. * An internal interface necessary for the implementation of {@link #retry()}. */ @FunctionalInterface interface IORunnable { void run() throws IOException; } private static final String OS_NAME_PROPERTY = "os.name"; /** * A simple check whether the underlying operating system is Windows. */ private static final boolean WINDOWS = System.getProperty(OS_NAME_PROPERTY) != null && System.getProperty(OS_NAME_PROPERTY).startsWith("Windows"); /** * How many times to retry a failing IO operation. */ private static final int MAX_RETRIES = WINDOWS ? 20 : 0; private FileUtils() { // utility class, forbidden constructor } public static void moveDir(Path src, Path dest) { try { moveDirPreferAtomic(src, dest); } catch (IOException e) { throw new IllegalStateException("Unable to move " + src + " to " + dest, e); } } private static void moveDirPreferAtomic(Path src, Path dest) throws IOException { try { retry(() -> Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE)); } catch (AtomicMoveNotSupportedException e) { // Fallback to non atomic move PathUtils.copyDirectory(src, dest); deleteRecursively(src); } } /** * Deletes recursively the specified file or directory tree. * * @param path */ public static void deleteRecursively(Path path) { if (!path.toFile().exists()) { return; } try { PathUtils.deleteDirectory(path); } catch (IOException e) { throw new IllegalStateException("Unable to delete directory " + path, e); } } /** * Creates a directory by creating all nonexistent parent directories first. * * @param path the directory to create */ public static void mkdirs(Path path) { try { Files.createDirectories(path); } catch (IOException e) { throw new IllegalStateException("Unable to create directory: " + path, e); } } /** * Populates a new temporary directory and when done, replace the target directory with it. * * @param dirContentUpdater function that will be called to create new content * @param target target location to replace when content is ready * @param work directory to populate with new content (typically a new empty temporary directory) */ public static void replaceDir(Consumer dirContentUpdater, Path target, Path work) { dirContentUpdater.accept(work); FileUtils.deleteRecursively(target); FileUtils.mkdirs(target.getParent()); FileUtils.moveDir(work, target); } /** * On Windows, retries the provided IO operation a number of times, in an effort to make the operation succeed. * * Operations that might fail on Windows are file & directory move, as well as file deletion. These failures * are typically caused by the virus scanner and/or the Windows Indexing Service. These services tend to open a file handle * on newly created files in an effort to scan their content. * * @param runnable the runnable whose execution should be retried */ static void retry(IORunnable runnable, int maxRetries) throws IOException { for (var retry = 0; retry < maxRetries; retry++) { try { runnable.run(); return; } catch (AccessDeniedException e) { // Sleep a bit to give a chance to the virus scanner / Windows Indexing Service to release the opened file handle try { Thread.sleep(100); } catch (InterruptedException ie) { // Nothing else that meaningfully can be done here Thread.currentThread().interrupt(); } } } // Give it a one last chance, and this time do not swallow the exception runnable.run(); } static void retry(IORunnable runnable) throws IOException { retry(runnable, MAX_RETRIES); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/HotspotDownloader.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Hotspots; import static java.util.function.Predicate.not; public class HotspotDownloader { private final Set enabledLanguages; public HotspotDownloader(Set enabledLanguages) { this.enabledLanguages = enabledLanguages; } /** * Fetch all hotspots of the project with specified key, using new SQ 10.1 api/issues/pull * * @param projectKey project key * @param branchName name of the branch. * @return List of hotspots. It can be empty but never null. */ public PullResult downloadFromPull(HotspotApi hotspotApi, String projectKey, String branchName, Optional lastSync, SonarLintCancelMonitor cancelMonitor) { var apiResult = hotspotApi.pullHotspots(projectKey, branchName, enabledLanguages, lastSync.map(Instant::toEpochMilli).orElse(null), cancelMonitor); var changedHotspots = apiResult.getHotspots() .stream() .filter(not(Hotspots.HotspotLite::getClosed)) .map(HotspotDownloader::convertLiteHotspot) .toList(); var closedIssueKeys = apiResult.getHotspots() .stream() .filter(Hotspots.HotspotLite::getClosed) .map(Hotspots.HotspotLite::getKey) .collect(Collectors.toSet()); return new PullResult(Instant.ofEpochMilli(apiResult.getTimestamp().getQueryTimestamp()), changedHotspots, closedIssueKeys); } private static ServerHotspot convertLiteHotspot(Hotspots.HotspotLite liteHotspotFromWs) { var creationDate = Instant.ofEpochMilli(liteHotspotFromWs.getCreationDate()); return new ServerHotspot( liteHotspotFromWs.getKey(), liteHotspotFromWs.getRuleKey(), liteHotspotFromWs.getMessage(), Path.of(liteHotspotFromWs.getFilePath()), toServerHotspotTextRange(liteHotspotFromWs.getTextRange()), creationDate, fromHotspotLite(liteHotspotFromWs), VulnerabilityProbability.valueOf(liteHotspotFromWs.getVulnerabilityProbability()), liteHotspotFromWs.getAssignee() ); } private static HotspotReviewStatus fromHotspotLite(Hotspots.HotspotLite hotspot) { var status = hotspot.getStatus(); var resolution = hotspot.hasResolution() ? hotspot.getResolution() : null; return HotspotReviewStatus.fromStatusAndResolution(status, resolution); } private static TextRangeWithHash toServerHotspotTextRange(Hotspots.TextRange textRange) { return new TextRangeWithHash( textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), textRange.getHash() ); } public static class PullResult { private final Instant queryTimestamp; private final List changedHotspots; private final Set closedHotspotKeys; public PullResult(Instant queryTimestamp, List changedHotspots, Set closedHotspotKeys) { this.queryTimestamp = queryTimestamp; this.changedHotspots = changedHotspots; this.closedHotspotKeys = closedHotspotKeys; } public Instant getQueryTimestamp() { return queryTimestamp; } public List getChangedHotspots() { return changedHotspots; } public Set getClosedHotspotKeys() { return closedHotspotKeys; } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/IssueDownloader.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonar.scanner.protocol.input.ScannerInput; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.IssueLite; import org.sonarsource.sonarlint.core.serverapi.rules.RulesApi; import org.sonarsource.sonarlint.core.serverconnection.issues.FileLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import static java.util.function.Predicate.not; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoImpactSeverity; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoSoftwareQuality; public class IssueDownloader { private final Set enabledLanguages; public Set getEnabledLanguages() { return enabledLanguages; } public IssueDownloader(Set enabledLanguages) { this.enabledLanguages = enabledLanguages; } /** * Fetch all issues of the component with specified key. * If the component doesn't exist or it exists but has no issues, an empty iterator is returned. * * @param key project key, or file key. * @param branchName name of the branch. * @param cancelMonitor * @return List of issues. It can be empty but never null. */ public List> downloadFromBatch(ServerApi serverApi, String key, @Nullable String branchName, SonarLintCancelMonitor cancelMonitor) { var issueApi = serverApi.issue(); List> result = new ArrayList<>(); var batchIssues = issueApi.downloadAllFromBatchIssues(key, branchName, cancelMonitor); for (ScannerInput.ServerIssue batchIssue : batchIssues) { // We ignore project level issues if (!RulesApi.TAINT_REPOS.contains(batchIssue.getRuleRepository()) && batchIssue.hasPath()) { result.add(convertBatchIssue(batchIssue)); } } return result; } /** * Fetch all issues of the project with specified key, using new SQ 9.6 api/issues/pull * * @param projectKey project key * @param branchName name of the branch. * @return List of issues. It can be empty but never null. */ public PullResult downloadFromPull(ServerApi serverApi, String projectKey, String branchName, Optional lastSync, SonarLintCancelMonitor cancelMonitor) { var issueApi = serverApi.issue(); var apiResult = issueApi.pullIssues(projectKey, branchName, enabledLanguages, lastSync.map(Instant::toEpochMilli).orElse(null), cancelMonitor); // Ignore project level issues List> changedIssues = apiResult.getIssues() .stream() // Ignore project level issues .filter(i -> i.getMainLocation().hasFilePath()) .filter(not(IssueLite::getClosed)) .>map(IssueDownloader::convertLiteIssue) .toList(); var closedIssueKeys = apiResult.getIssues() .stream() // Ignore project level issues .filter(i -> i.getMainLocation().hasFilePath()) .filter(IssueLite::getClosed) .map(IssueLite::getKey) .collect(Collectors.toSet()); return new PullResult(Instant.ofEpochMilli(apiResult.getTimestamp().getQueryTimestamp()), changedIssues, closedIssueKeys); } private static ServerIssue convertBatchIssue(ScannerInput.ServerIssue batchIssueFromWs) { var ruleKey = batchIssueFromWs.getRuleRepository() + ":" + batchIssueFromWs.getRuleKey(); // We have filtered out issues without file path earlier var filePath = Path.of(batchIssueFromWs.getPath()); var creationDate = Instant.ofEpochMilli(batchIssueFromWs.getCreationDate()); var userSeverity = batchIssueFromWs.getManualSeverity() ? IssueSeverity.valueOf(batchIssueFromWs.getSeverity().name()) : null; var ruleType = RuleType.valueOf(batchIssueFromWs.getType()); var impacts = Collections.emptyMap(); var resolutionStatus = IssueStatus.parse(batchIssueFromWs.getResolution()); if (batchIssueFromWs.hasLine()) { return new LineLevelServerIssue(batchIssueFromWs.getKey(), batchIssueFromWs.hasResolution(), resolutionStatus, ruleKey, batchIssueFromWs.getMsg(), batchIssueFromWs.getChecksum(), filePath, creationDate, userSeverity, ruleType, batchIssueFromWs.getLine(), impacts); } else { return new FileLevelServerIssue(batchIssueFromWs.getKey(), batchIssueFromWs.hasResolution(), resolutionStatus, ruleKey, batchIssueFromWs.getMsg(), filePath, creationDate, userSeverity, ruleType, impacts); } } private static ServerIssue convertLiteIssue(IssueLite liteIssueFromWs) { var mainLocation = liteIssueFromWs.getMainLocation(); // We have filtered out issues without file path earlier var filePath = Path.of(mainLocation.getFilePath()); var creationDate = Instant.ofEpochMilli(liteIssueFromWs.getCreationDate()); var userSeverity = liteIssueFromWs.hasUserSeverity() ? IssueSeverity.valueOf(liteIssueFromWs.getUserSeverity().name()) : null; var ruleType = RuleType.valueOf(liteIssueFromWs.getType().name()); var impacts = liteIssueFromWs.getImpactsList().stream() .map(i -> Map.entry( parseProtoSoftwareQuality(i), parseProtoImpactSeverity(i))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (mainLocation.hasTextRange()) { return new RangeLevelServerIssue(liteIssueFromWs.getKey(), liteIssueFromWs.getResolved(), null, liteIssueFromWs.getRuleKey(), mainLocation.getMessage(), filePath, creationDate, userSeverity, ruleType, toServerIssueTextRange(mainLocation.getTextRange()), impacts); } else { return new FileLevelServerIssue(liteIssueFromWs.getKey(), liteIssueFromWs.getResolved(), null, liteIssueFromWs.getRuleKey(), mainLocation.getMessage(), filePath, creationDate, userSeverity, ruleType, impacts); } } private static TextRangeWithHash toServerIssueTextRange(Issues.TextRange textRange) { return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), textRange.getHash()); } public static class PullResult { private final Instant queryTimestamp; private final List> changedIssues; private final Set closedIssueKeys; public PullResult(Instant queryTimestamp, List> changedIssues, Set closedIssueKeys) { this.queryTimestamp = queryTimestamp; this.changedIssues = changedIssues; this.closedIssueKeys = closedIssueKeys; } public Instant getQueryTimestamp() { return queryTimestamp; } public List> getChangedIssues() { return changedIssues; } public Set getClosedIssueKeys() { return closedIssueKeys; } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/IssueStorePaths.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.nio.file.Paths; import javax.annotation.CheckForNull; import static org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils.toSonarQubePath; public class IssueStorePaths { private IssueStorePaths() { } @CheckForNull public static String idePathToFileKey(ProjectBinding projectBinding, Path ideFilePath) { var serverFilePath = idePathToServerPath(projectBinding, ideFilePath); if (serverFilePath == null) { return null; } return componentKey(projectBinding, serverFilePath); } public static String componentKey(ProjectBinding projectBinding, Path serverFilePath) { return componentKey(projectBinding.projectKey(), serverFilePath); } public static String componentKey(String projectKey, Path serverFilePath) { return projectKey + ":" + toSonarQubePath(serverFilePath); } @CheckForNull public static Path idePathToServerPath(ProjectBinding projectBinding, Path ideFilePath) { return idePathToServerPath(Paths.get(projectBinding.idePathPrefix()), Paths.get(projectBinding.serverPathPrefix()), ideFilePath); } @CheckForNull public static Path idePathToServerPath(Path idePathPrefix, Path serverPathPrefix, Path ideFilePath) { Path commonPart; if (!idePathPrefix.toString().isEmpty()) { if (!ideFilePath.startsWith(idePathPrefix)) { return null; } commonPart = idePathPrefix.relativize(ideFilePath); } else { commonPart = ideFilePath; } if (!serverPathPrefix.toString().isEmpty()) { return serverPathPrefix.resolve(commonPart); } else { return commonPart; } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/LocalStorageSynchronizer.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import com.google.common.collect.Maps; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.qualityprofile.QualityProfile; import org.sonarsource.sonarlint.core.serverconnection.storage.StorageException; import static java.util.stream.Collectors.toSet; public class LocalStorageSynchronizer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Set enabledLanguageKeys; private final ConnectionStorage storage; private final ServerInfoSynchronizer serverInfoSynchronizer; public LocalStorageSynchronizer(Set enabledLanguages, ServerInfoSynchronizer serverInfoSynchronizer, ConnectionStorage storage) { this.enabledLanguageKeys = enabledLanguages.stream().map(SonarLanguage::getSonarLanguageKey).collect(toSet()); this.storage = storage; this.serverInfoSynchronizer = serverInfoSynchronizer; } public Summary synchronizeServerInfosAndPlugins(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { serverInfoSynchronizer.synchronize(serverApi, cancelMonitor); var version = storage.serverInfo().read().orElseThrow().version(); return new Summary(version); } private static AnalyzerSettingsUpdateSummary diffAnalyzerConfiguration(AnalyzerConfiguration original, AnalyzerConfiguration updated) { var originalSettings = original.getSettings().getAll(); var updatedSettings = updated.getSettings().getAll(); var diff = Maps.difference(originalSettings, updatedSettings); var updatedSettingsValueByKey = diff.entriesDiffering().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().rightValue())); updatedSettingsValueByKey.putAll(diff.entriesOnlyOnRight()); updatedSettingsValueByKey.putAll(diff.entriesOnlyOnLeft().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> ""))); return new AnalyzerSettingsUpdateSummary(updatedSettingsValueByKey); } public AnalyzerSettingsUpdateSummary synchronizeAnalyzerConfig(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { var updatedAnalyzerConfiguration = downloadAnalyzerConfig(serverApi, projectKey, cancelMonitor); AnalyzerSettingsUpdateSummary configUpdateSummary; try { var originalAnalyzerConfiguration = storage.project(projectKey).analyzerConfiguration().read(); configUpdateSummary = diffAnalyzerConfiguration(originalAnalyzerConfiguration, updatedAnalyzerConfiguration); } catch (StorageException e) { configUpdateSummary = new AnalyzerSettingsUpdateSummary(updatedAnalyzerConfiguration.getSettings().getAll()); } storage.project(projectKey).analyzerConfiguration().store(updatedAnalyzerConfiguration); var version = storage.serverInfo().read().orElseThrow().version(); serverApi.newCodeApi().getNewCodeDefinition(projectKey, null, version, cancelMonitor) .ifPresent(ncd -> storage.project(projectKey).newCodeDefinition().store(ncd)); return configUpdateSummary; } private AnalyzerConfiguration downloadAnalyzerConfig(ServerApi serverApi, String projectKey, SonarLintCancelMonitor cancelMonitor) { LOG.info("[SYNC] Synchronizing analyzer configuration for project '{}'", projectKey); LOG.info("[SYNC] Languages enabled for synchronization: {}", enabledLanguageKeys); Map currentRuleSets; int currentSchemaVersion; try { var analyzerConfiguration = storage.project(projectKey).analyzerConfiguration().read(); currentRuleSets = analyzerConfiguration.getRuleSetByLanguageKey(); currentSchemaVersion = analyzerConfiguration.getSchemaVersion(); } catch (StorageException e) { currentRuleSets = Map.of(); currentSchemaVersion = 0; } var shouldForceRuleSetUpdate = outdatedSchema(currentSchemaVersion); var currentRuleSetsFinal = currentRuleSets; var settings = new Settings(serverApi.settings().getProjectSettings(projectKey, cancelMonitor)); var ruleSetsByLanguageKey = serverApi.qualityProfile().getQualityProfiles(projectKey, cancelMonitor).stream() .filter(qualityProfile -> enabledLanguageKeys.contains(qualityProfile.getLanguage())) .collect(Collectors.toMap(QualityProfile::getLanguage, profile -> toRuleSet(serverApi, currentRuleSetsFinal, profile, shouldForceRuleSetUpdate, cancelMonitor))); return new AnalyzerConfiguration(settings, ruleSetsByLanguageKey, AnalyzerConfiguration.CURRENT_SCHEMA_VERSION); } private static RuleSet toRuleSet(ServerApi serverApi, Map currentRuleSets, QualityProfile profile, boolean forceUpdate, SonarLintCancelMonitor cancelMonitor) { var language = profile.getLanguage(); if (forceUpdate || newlySupportedLanguage(currentRuleSets, language) || profileModifiedSinceLastSync(currentRuleSets, profile, language)) { var profileKey = profile.getKey(); LOG.info("[SYNC] Fetching rule set for language '{}' from profile '{}'", language, profileKey); var profileActiveRules = serverApi.rules().getAllActiveRules(profileKey, cancelMonitor); return new RuleSet(profileActiveRules, profile.getRulesUpdatedAt()); } else { LOG.info("[SYNC] Active rules for '{}' are up-to-date", language); return currentRuleSets.get(language); } } private static boolean profileModifiedSinceLastSync(Map currentRuleSets, QualityProfile profile, String language) { return !currentRuleSets.get(language).getLastModified().equals(profile.getRulesUpdatedAt()); } private static boolean newlySupportedLanguage(Map currentRuleSets, String language) { return !currentRuleSets.containsKey(language); } private static boolean outdatedSchema(int currentSchemaVersion) { return currentSchemaVersion < AnalyzerConfiguration.CURRENT_SCHEMA_VERSION; } public record Summary(Version version) { } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/Organization.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.UUID; public record Organization(String id, UUID uuidV4) { } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/OrganizationSynchronizer.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; public class OrganizationSynchronizer { private final ConnectionStorage storage; public OrganizationSynchronizer(ConnectionStorage storage) { this.storage = storage; } // should be called only in the context of SonarQube Cloud public Organization readOrSynchronizeOrganization(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { return storage.organization().read() .orElseGet(() -> synchronize(serverApi, cancelMonitor)); } private Organization synchronize(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { var organizationDto = serverApi.organization().getOrganizationByKey(cancelMonitor); var organization = new Organization(organizationDto.id(), organizationDto.uuidV4()); storage.organization().store(organization); return organization; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ProjectBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Objects; import java.util.Optional; /** * Describes the link between a project in the IDE and a project in SonarQube/SonarCloud. * */ public class ProjectBinding { private final String projectKey; private final String serverPathPrefix; private final String idePathPrefix; public ProjectBinding(String projectKey, String serverPathPrefix, String idePathPrefix) { this.projectKey = projectKey; this.serverPathPrefix = serverPathPrefix; this.idePathPrefix = idePathPrefix; } public String projectKey() { return projectKey; } public String serverPathPrefix() { return serverPathPrefix; } public String idePathPrefix() { return idePathPrefix; } public Optional serverPathToIdePath(String serverPath) { if (!serverPath.startsWith(serverPathPrefix())) { return Optional.empty(); } var localPrefixLen = serverPathPrefix().length(); if (localPrefixLen > 0) { localPrefixLen++; } var actualLocalPrefix = idePathPrefix(); if (!actualLocalPrefix.isEmpty()) { actualLocalPrefix = actualLocalPrefix + "/"; } return Optional.of(actualLocalPrefix + serverPath.substring(localPrefixLen)); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } var that = (ProjectBinding) o; return Objects.equals(projectKey, that.projectKey) && Objects.equals(serverPathPrefix, that.serverPathPrefix) && Objects.equals(idePathPrefix, that.idePathPrefix); } @Override public int hashCode() { return Objects.hash(projectKey, serverPathPrefix, idePathPrefix); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ProjectBranches.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Set; public class ProjectBranches { private final Set branchNames; private final String mainBranchName; public ProjectBranches(Set branchNames, String mainBranchName) { this.branchNames = branchNames; this.mainBranchName = mainBranchName; } public Set getBranchNames() { return branchNames; } public String getMainBranchName() { return mainBranchName; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ProjectBranchesStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil; import org.sonarsource.sonarlint.core.serverconnection.storage.RWLock; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class ProjectBranchesStorage { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public ProjectBranchesStorage(Path projectStorageRoot) { this.storageFilePath = projectStorageRoot.resolve("project_branches.pb"); } public boolean exists() { return Files.exists(storageFilePath); } public void store(ProjectBranches projectBranches) { FileUtils.mkdirs(storageFilePath.getParent()); var data = adapt(projectBranches); LOG.debug("Storing project branches in {}", storageFilePath); rwLock.write(() -> writeToFile(data, storageFilePath)); } public ProjectBranches read() { return adapt(rwLock.read(() -> ProtobufFileUtil.readFile(storageFilePath, Sonarlint.ProjectBranches.parser()))); } private static ProjectBranches adapt(Sonarlint.ProjectBranches projectBranches) { return new ProjectBranches(Set.copyOf(projectBranches.getBranchNameList()), projectBranches.getMainBranchName()); } private static Sonarlint.ProjectBranches adapt(ProjectBranches projectBranches) { return Sonarlint.ProjectBranches.newBuilder() .addAllBranchName(projectBranches.getBranchNames()) .setMainBranchName(projectBranches.getMainBranchName()) .build(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/RuleSet.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Collection; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.serverapi.rules.ServerActiveRule; public class RuleSet { private final Collection rules; private final Map rulesByKey; private final String lastModified; public RuleSet(Collection rules, String lastModified) { this.rules = rules; this.rulesByKey = rules.stream().collect(Collectors.toMap(ServerActiveRule::getRuleKey, Function.identity())); this.lastModified = lastModified; } public Collection getRules() { return rules; } public Map getRulesByKey() { return rulesByKey; } public String getLastModified() { return lastModified; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerHotspotUpdater.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.util.Set; import java.util.function.Supplier; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import static org.sonarsource.sonarlint.core.serverconnection.ServerUpdaterUtils.computeLastSync; public class ServerHotspotUpdater { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConnectionStorage storage; private final HotspotDownloader hotspotDownloader; public ServerHotspotUpdater(ConnectionStorage storage, HotspotDownloader hotspotDownloader) { this.storage = storage; this.hotspotDownloader = hotspotDownloader; } public void updateAll(HotspotApi hotspotApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { var projectHotspots = hotspotApi.getAll(projectKey, branchName, cancelMonitor); storage.project(projectKey).findings().replaceAllHotspotsOfBranch(branchName, projectHotspots, enabledLanguages); } public void updateForFile(HotspotApi hotspotApi, String projectKey, Path serverFilePath, String branchName, Supplier serverVersionSupplier, SonarLintCancelMonitor cancelMonitor) { if (hotspotApi.supportHotspotsPull(serverVersionSupplier)) { LOG.debug("Skip downloading file hotspots on SonarQube 10.1+"); return; } var fileHotspots = hotspotApi.getFromFile(projectKey, serverFilePath, branchName, cancelMonitor); storage.project(projectKey).findings().replaceAllHotspotsOfFile(branchName, serverFilePath, fileHotspots); } public void sync(HotspotApi hotspotApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { var lastSync = storage.project(projectKey).findings().getLastHotspotSyncTimestamp(branchName); lastSync = computeLastSync(enabledLanguages, lastSync, storage.project(projectKey).findings().getLastHotspotEnabledLanguages(branchName)); var result = hotspotDownloader.downloadFromPull(hotspotApi, projectKey, branchName, lastSync, cancelMonitor); storage.project(projectKey).findings().mergeHotspots(branchName, result.getChangedHotspots(), result.getClosedHotspotKeys(), result.getQueryTimestamp(), enabledLanguages); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerInfoSynchronizer.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Set; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.features.Feature; public class ServerInfoSynchronizer { private final ConnectionStorage storage; public ServerInfoSynchronizer(ConnectionStorage storage) { this.storage = storage; } public StoredServerInfo readOrSynchronizeServerInfo(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { return storage.serverInfo().read() .orElseGet(() -> { synchronize(serverApi, cancelMonitor); return storage.serverInfo().read().get(); }); } public void synchronize(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { var serverStatus = serverApi.system().getStatus(cancelMonitor); var serverVersionAndStatusChecker = new ServerVersionAndStatusChecker(serverApi); serverVersionAndStatusChecker.checkVersionAndStatus(cancelMonitor); var globalSettings = serverApi.settings().getGlobalSettings(cancelMonitor); var supportedFeatures = serverApi.isSonarCloud() ? getSupportedFeaturesForSonarQubeCloud(serverApi, cancelMonitor) : serverApi.features().list(cancelMonitor); storage.serverInfo().store(serverStatus, supportedFeatures, globalSettings); } private static Set getSupportedFeaturesForSonarQubeCloud(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { return serverApi.sca().isScaEnabled(cancelMonitor).enabled() ? Set.of(Feature.SCA) : Set.of(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerIssueUpdater.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.serverconnection.storage.UpdateSummary; import static java.util.stream.Collectors.toSet; import static org.sonarsource.sonarlint.core.serverconnection.ServerUpdaterUtils.computeLastSync; public class ServerIssueUpdater { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConnectionStorage storage; private final IssueDownloader issueDownloader; private final TaintIssueDownloader taintIssueDownloader; public ServerIssueUpdater(ConnectionStorage storage, IssueDownloader issueDownloader, TaintIssueDownloader taintIssueDownloader) { this.storage = storage; this.issueDownloader = issueDownloader; this.taintIssueDownloader = taintIssueDownloader; } public void update(ServerApi serverApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { if (serverApi.isSonarCloud()) { var issues = issueDownloader.downloadFromBatch(serverApi, projectKey, branchName, cancelMonitor); storage.project(projectKey).findings().replaceAllIssuesOfBranch(branchName, issues, enabledLanguages); } else { sync(serverApi, projectKey, branchName, issueDownloader.getEnabledLanguages(), cancelMonitor); } } public void sync(ServerApi serverApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { var lastSync = storage.project(projectKey).findings().getLastIssueSyncTimestamp(branchName); lastSync = computeLastSync(enabledLanguages, lastSync, storage.project(projectKey).findings().getLastIssueEnabledLanguages(branchName)); var result = issueDownloader.downloadFromPull(serverApi, projectKey, branchName, lastSync, cancelMonitor); storage.project(projectKey).findings().mergeIssues(branchName, result.getChangedIssues(), result.getClosedIssueKeys(), result.getQueryTimestamp(), enabledLanguages); } public UpdateSummary syncTaints(ServerApi serverApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { var serverIssueStore = storage.project(projectKey).findings(); var lastSync = serverIssueStore.getLastTaintSyncTimestamp(branchName); lastSync = computeLastSync(enabledLanguages, lastSync, storage.project(projectKey).findings().getLastTaintEnabledLanguages(branchName)); var result = taintIssueDownloader.downloadTaintFromPull(serverApi, projectKey, branchName, lastSync, cancelMonitor); var previousTaintIssues = serverIssueStore.loadTaint(branchName); var previousTaintIssueKeys = previousTaintIssues.stream().map(ServerTaintIssue::getSonarServerKey).collect(toSet()); serverIssueStore.mergeTaintIssues(branchName, result.getChangedTaintIssues(), result.getClosedIssueKeys(), result.getQueryTimestamp(), enabledLanguages); var deletedTaintVulnerabilityIds = previousTaintIssues.stream().filter(issue -> result.getClosedIssueKeys().contains(issue.getSonarServerKey())).map(ServerTaintIssue::getId) .collect(toSet()); var addedTaintVulnerabilities = result.getChangedTaintIssues().stream().filter(issue -> !previousTaintIssueKeys.contains(issue.getSonarServerKey())) .toList(); var updatedTaintVulnerabilities = result.getChangedTaintIssues().stream().filter(issue -> previousTaintIssueKeys.contains(issue.getSonarServerKey())) .toList(); return new UpdateSummary<>(deletedTaintVulnerabilityIds, addedTaintVulnerabilities, updatedTaintVulnerabilities); } public void updateFileIssuesIfNeeded(ServerApi serverApi, String projectKey, Path serverFileRelativePath, String branchName, SonarLintCancelMonitor cancelMonitor) { if (serverApi.isSonarCloud()) { updateFileIssues(serverApi, projectKey, serverFileRelativePath, branchName, cancelMonitor); } else { LOG.debug("Skip downloading file issues on SonarQube "); } } public void updateFileIssues(ServerApi serverApi, String projectKey, Path serverFileRelativePath, String branchName, SonarLintCancelMonitor cancelMonitor) { var fileKey = IssueStorePaths.componentKey(projectKey, serverFileRelativePath); List> issues = new ArrayList<>(); try { issues.addAll(issueDownloader.downloadFromBatch(serverApi, fileKey, branchName, cancelMonitor)); } catch (Exception e) { // null as cause so that it doesn't get wrapped throw new DownloadException("Failed to update file issues: " + e.getMessage(), null); } storage.project(projectKey).findings().replaceAllIssuesOfFile(branchName, serverFileRelativePath, issues); } public UpdateSummary downloadProjectTaints(ServerApi serverApi, String projectKey, String branchName, Set enabledLanguages, SonarLintCancelMonitor cancelMonitor) { List newTaintIssues; try { newTaintIssues = new ArrayList<>(taintIssueDownloader.downloadTaintFromIssueSearch(serverApi, projectKey, branchName, cancelMonitor)); } catch (Exception e) { // null as cause so that it doesn't get wrapped throw new DownloadException("Failed to update file taint vulnerabilities: " + e.getMessage(), null); } var findingsStorage = storage.project(projectKey).findings(); var previousTaintIssues = findingsStorage.loadTaint(branchName); var previousTaintIssueKeys = previousTaintIssues.stream().map(ServerTaintIssue::getSonarServerKey).collect(toSet()); findingsStorage.replaceAllTaintsOfBranch(branchName, newTaintIssues, enabledLanguages); var newTaintIssueKeys = newTaintIssues.stream().map(ServerTaintIssue::getSonarServerKey).collect(toSet()); var deletedTaintVulnerabilityIds = previousTaintIssues.stream().filter(issue -> !newTaintIssueKeys.contains(issue.getSonarServerKey())).map(ServerTaintIssue::getId) .collect(toSet()); var addedTaintVulnerabilities = newTaintIssues.stream().filter(issue -> !previousTaintIssueKeys.contains(issue.getSonarServerKey())) .toList(); var updatedTaintVulnerabilities = newTaintIssues.stream().filter(issue -> previousTaintIssueKeys.contains(issue.getSonarServerKey())) .toList(); return new UpdateSummary<>(deletedTaintVulnerabilityIds, addedTaintVulnerabilities, updatedTaintVulnerabilities); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerSettings.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Map; import java.util.Optional; public record ServerSettings(Map globalSettings) { public static final String MQR_MODE_SETTING = "sonar.multi-quality-mode.enabled"; public static final String EARLY_ACCESS_MISRA_ENABLED = "sonar.earlyAccess.misra.enabled"; public static final String MISRA_COMPLIANCE_ENABLED = "sonar.misracompliance.enabled"; public Optional getAsBoolean(String settingKey) { return Optional.ofNullable(globalSettings.get(settingKey)) .map(Boolean::valueOf); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerUpdaterUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.time.Instant; import java.util.Optional; import java.util.Set; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class ServerUpdaterUtils { private ServerUpdaterUtils() { // utility class } /** * @return empty if there is no exact match for languages to indicate all issues must be fetched */ public static Optional computeLastSync(Set enabledLanguages, Optional lastSync, Set lastEnabledLanguages) { if (lastEnabledLanguages.isEmpty() || (!lastEnabledLanguages.equals(enabledLanguages))) { lastSync = Optional.empty(); } return lastSync; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/ServerVersionAndStatusChecker.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.UnsupportedServerException; import org.sonarsource.sonarlint.core.serverapi.system.ServerStatusInfo; import org.sonarsource.sonarlint.core.serverapi.system.SystemApi; public class ServerVersionAndStatusChecker { private static final String MIN_SQ_VERSION = "9.9"; private static final String MIN_SQ_VERSION_SUPPORTING_BEARER = "10.4"; private final SystemApi systemApi; private final boolean isSonarCloud; public ServerVersionAndStatusChecker(ServerApi serverApi) { this.systemApi = serverApi.system(); this.isSonarCloud = serverApi.isSonarCloud(); } /** * Checks SonarQube availability status and version against the minimum version supported by the core * or only server availability status for SonarCloud * * @throws UnsupportedServerException if version < minimum supported version * @throws IllegalStateException If server is not ready */ public void checkVersionAndStatus(SonarLintCancelMonitor cancelMonitor) { var serverStatus = systemApi.getStatus(cancelMonitor); if (isSonarCloud) { checkServerUp(serverStatus); } else { checkServerUpAndSupported(serverStatus); } } public boolean isSupportingBearer(ServerStatusInfo serverStatus) { if (isSonarCloud) { return true; } else { var serverVersion = Version.create(serverStatus.version()); return serverVersion.compareToIgnoreQualifier(Version.create(MIN_SQ_VERSION_SUPPORTING_BEARER)) >= 0; } } private static void checkServerUp(ServerStatusInfo serverStatus) { if (!serverStatus.isUp()) { throw new IllegalStateException(serverNotReady(serverStatus)); } } private static void checkServerUpAndSupported(ServerStatusInfo serverStatus) { checkServerUp(serverStatus); var serverVersion = Version.create(serverStatus.version()); if (serverVersion.compareToIgnoreQualifier(Version.create(MIN_SQ_VERSION)) < 0) { throw new UnsupportedServerException(unsupportedVersion(serverStatus)); } } private static String unsupportedVersion(ServerStatusInfo serverStatus) { return "Your SonarQube Server instance has version " + serverStatus.version() + ". Version should be greater or equal to " + MIN_SQ_VERSION; } private static String serverNotReady(ServerStatusInfo serverStatus) { return "Server not ready (" + serverStatus.status() + ")"; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/Settings.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Map; public class Settings { private final Map settings; public Settings(Map settings) { this.settings = settings; } public Map getAll() { return settings; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/SonarProjectStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import org.sonarsource.sonarlint.core.serverconnection.storage.NewCodeDefinitionStorage; import org.sonarsource.sonarlint.core.serverconnection.storage.ProjectServerIssueStore; import org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueStoresManager; import org.sonarsource.sonarlint.core.serverconnection.storage.SmartNotificationsStorage; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs; public class SonarProjectStorage { private final ServerIssueStoresManager serverIssueStoresManager; private final String sonarProjectKey; private final AnalyzerConfigurationStorage analyzerConfigurationStorage; private final ProjectBranchesStorage projectBranchesStorage; private final SmartNotificationsStorage smartNotificationsStorage; private final NewCodeDefinitionStorage newCodeDefinitionStorage; private final Path projectStorageRoot; public SonarProjectStorage(Path projectsStorageRoot, ServerIssueStoresManager serverIssueStoresManager, String sonarProjectKey) { this.projectStorageRoot = projectsStorageRoot.resolve(encodeForFs(sonarProjectKey)); this.serverIssueStoresManager = serverIssueStoresManager; this.sonarProjectKey = sonarProjectKey; this.analyzerConfigurationStorage = new AnalyzerConfigurationStorage(projectStorageRoot); this.projectBranchesStorage = new ProjectBranchesStorage(projectStorageRoot); this.smartNotificationsStorage = new SmartNotificationsStorage(projectStorageRoot); this.newCodeDefinitionStorage = new NewCodeDefinitionStorage(projectStorageRoot); } public ProjectServerIssueStore findings() { return serverIssueStoresManager.get(sonarProjectKey); } public AnalyzerConfigurationStorage analyzerConfiguration() { return analyzerConfigurationStorage; } public ProjectBranchesStorage branches() { return projectBranchesStorage; } public SmartNotificationsStorage smartNotifications() { return smartNotificationsStorage; } public NewCodeDefinitionStorage newCodeDefinition() { return newCodeDefinitionStorage; } public Path filePath() { return projectStorageRoot; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/SonarServerSettingsChangedEvent.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Map; import java.util.Set; public record SonarServerSettingsChangedEvent(String connectionId, Set configScopeIds, Map updatedSettingsValueByKey) { } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/StoredPlugin.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; public class StoredPlugin { private final String key; private final String hash; private final Path jarPath; public StoredPlugin(String key, String hash, Path jarPath) { this.key = key; this.hash = hash; this.jarPath = jarPath; } public String getKey() { return key; } public String getHash() { return hash; } public Path getJarPath() { return jarPath; } public boolean hasSameHash(ServerPlugin serverPlugin) { return getHash().equals(serverPlugin.getHash()); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/StoredServerInfo.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.util.Set; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import static org.sonarsource.sonarlint.core.serverconnection.ServerSettings.MQR_MODE_SETTING; public record StoredServerInfo(Version version, Set features, ServerSettings globalSettings, String serverId) { private static final String MIN_MQR_MODE_SUPPORT_VERSION = "10.2"; private static final String MQR_MODE_SETTING_MIN_VERSION = "10.8"; public boolean shouldConsiderMultiQualityModeEnabled() { if (version.satisfiesMinRequirement(Version.create(MQR_MODE_SETTING_MIN_VERSION))) { // starting 10.8, the sonar.multi-quality-mode.enabled setting was introduced. We honor this setting in priority return globalSettings.getAsBoolean(MQR_MODE_SETTING).orElse(false); } // if no setting is present, MQR mode should be used for 10.2+, otherwise standard mode should be used return version.satisfiesMinRequirement(Version.create(MIN_MQR_MODE_SUPPORT_VERSION)); } public boolean hasFeature(Feature feature) { return features.contains(feature); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/SynchronizationResult.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; public class SynchronizationResult { private final boolean analyzerUpdated; public SynchronizationResult(boolean analyzerUpdated) { this.analyzerUpdated = analyzerUpdated; } public boolean hasAnalyzerBeenUpdated() { return analyzerUpdated; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/TaintIssueDownloader.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import com.google.common.annotations.VisibleForTesting; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleKey; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Flow; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.TextRange; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.Issue; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.TaintVulnerabilityLite; import org.sonarsource.sonarlint.core.serverapi.source.SourceApi; import org.sonarsource.sonarlint.core.serverapi.util.ServerApiUtils; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import static java.util.function.Predicate.not; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoImpactSeverity; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoSoftwareQuality; public class TaintIssueDownloader { private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s"); private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Set enabledLanguages; public TaintIssueDownloader(Set enabledLanguages) { this.enabledLanguages = enabledLanguages; } public List downloadTaintFromIssueSearch(ServerApi serverApi, String key, @Nullable String branchName, SonarLintCancelMonitor cancelMonitor) { var issueApi = serverApi.issue(); List result = new ArrayList<>(); Set taintRuleKeys = serverApi.rules().getAllTaintRules(List.of(SonarLanguage.values()), cancelMonitor); Map sourceCodeByKey = new HashMap<>(); var downloadVulnerabilitiesForRules = issueApi.downloadVulnerabilitiesForRules(key, taintRuleKeys, branchName, cancelMonitor); downloadVulnerabilitiesForRules.getIssues() .stream() .map(i -> convertTaintVulnerability(serverApi.source(), i, downloadVulnerabilitiesForRules.getComponentPathsByKey(), sourceCodeByKey, cancelMonitor)) .filter(Objects::nonNull) .forEach(result::add); return result; } /** * Fetch all taint issues of the project with specified key, using new SQ 9.6 api/issues/pull_taint * * @param projectKey project key * @param branchName name of the branch. * @return List of issues. It can be empty but never null. */ public PullTaintResult downloadTaintFromPull(ServerApi serverApi, String projectKey, String branchName, Optional lastSync, SonarLintCancelMonitor cancelMonitor) { var issueApi = serverApi.issue(); var apiResult = issueApi.pullTaintIssues(projectKey, branchName, enabledLanguages, lastSync.map(Instant::toEpochMilli).orElse(null), cancelMonitor); var changedIssues = apiResult.getTaintIssues() .stream() // Ignore project level issues .filter(i -> i.getMainLocation().hasFilePath()) .filter(not(TaintVulnerabilityLite::getClosed)) .map(TaintIssueDownloader::convertLiteTaintIssue) .toList(); var closedIssueKeys = apiResult.getTaintIssues() .stream() // Ignore project level issues .filter(i -> i.getMainLocation().hasFilePath()) .filter(TaintVulnerabilityLite::getClosed) .map(TaintVulnerabilityLite::getKey) .collect(Collectors.toSet()); return new PullTaintResult(Instant.ofEpochMilli(apiResult.getTimestamp().getQueryTimestamp()), changedIssues, closedIssueKeys); } @CheckForNull private static ServerTaintIssue convertTaintVulnerability(SourceApi sourceApi, Issue taintVulnerabilityFromWs, Map componentPathsByKey, Map sourceCodeByKey, SonarLintCancelMonitor cancelMonitor) { var ruleKey = RuleKey.parse(taintVulnerabilityFromWs.getRule()); var primaryLocation = convertPrimaryLocation(sourceApi, taintVulnerabilityFromWs, componentPathsByKey, sourceCodeByKey, cancelMonitor); var filePath = primaryLocation.filePath(); if (filePath == null) { // Ignore project level issues return null; } var ruleDescriptionContextKey = taintVulnerabilityFromWs.hasRuleDescriptionContextKey() ? taintVulnerabilityFromWs.getRuleDescriptionContextKey() : null; var cleanCodeAttribute = parseProtoCleanCodeAttribute(taintVulnerabilityFromWs); var impacts = taintVulnerabilityFromWs.getImpactsList().stream() .map(i -> Map.entry(parseProtoSoftwareQuality(i), parseProtoImpactSeverity(i))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); var resolution = taintVulnerabilityFromWs.getResolution(); var resolutionStatus = IssueStatus.parse(resolution); return new ServerTaintIssue( UUID.randomUUID(), taintVulnerabilityFromWs.getKey(), !resolution.isEmpty(), resolutionStatus, ruleKey.toString(), primaryLocation.message(), filePath, ServerApiUtils.parseOffsetDateTime(taintVulnerabilityFromWs.getCreationDate()).toInstant(), IssueSeverity.valueOf(taintVulnerabilityFromWs.getSeverity().name()), RuleType.valueOf(taintVulnerabilityFromWs.getType().name()), primaryLocation.textRange(), ruleDescriptionContextKey, cleanCodeAttribute, impacts, convertFlows(sourceApi, taintVulnerabilityFromWs.getFlowsList(), componentPathsByKey, sourceCodeByKey, cancelMonitor)); } @CheckForNull @VisibleForTesting static CleanCodeAttribute parseProtoCleanCodeAttribute(Issue taintVulnerabilityFromWs) { if (!taintVulnerabilityFromWs.hasCleanCodeAttribute() || taintVulnerabilityFromWs.getCleanCodeAttribute() == Common.CleanCodeAttribute.UNKNOWN_ATTRIBUTE) { return null; } return CleanCodeAttribute.valueOf(taintVulnerabilityFromWs.getCleanCodeAttribute().name()); } @CheckForNull @VisibleForTesting static CleanCodeAttribute parseProtoCleanCodeAttribute(TaintVulnerabilityLite taintVulnerabilityFromWs) { if (!taintVulnerabilityFromWs.hasCleanCodeAttribute() || taintVulnerabilityFromWs.getCleanCodeAttribute() == Common.CleanCodeAttribute.UNKNOWN_ATTRIBUTE) { return null; } return CleanCodeAttribute.valueOf(taintVulnerabilityFromWs.getCleanCodeAttribute().name()); } private static List convertFlows(SourceApi sourceApi, List flowsList, Map componentPathsByKey, Map sourceCodeByKey, SonarLintCancelMonitor cancelMonitor) { return flowsList.stream() .map(flowFromWs -> new ServerTaintIssue.Flow(flowFromWs.getLocationsList().stream().map(locationFromWs -> { var componentPath = componentPathsByKey.get(locationFromWs.getComponent()); if (locationFromWs.hasTextRange()) { var codeSnippet = getCodeSnippet(sourceApi, locationFromWs.getComponent(), locationFromWs.getTextRange(), sourceCodeByKey, cancelMonitor); String textRangeHash; if (codeSnippet != null) { textRangeHash = hash(codeSnippet); } else { // Use empty String, the client will detect a mismatch with real hash and apply UX for mismatched locations textRangeHash = ""; } return new ServerTaintIssue.ServerIssueLocation(componentPath, convertTextRangeFromWs(locationFromWs.getTextRange(), textRangeHash), locationFromWs.getMsg()); } return new ServerTaintIssue.ServerIssueLocation(componentPath, null, locationFromWs.getMsg()); }).toList())) .toList(); } private static TextRangeWithHash toServerTaintIssueTextRange(Issues.TextRange textRange) { return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), textRange.getHash()); } private static ServerTaintIssue convertLiteTaintIssue(TaintVulnerabilityLite liteTaintIssueFromWs) { var mainLocation = liteTaintIssueFromWs.getMainLocation(); // We have filtered out issues without file path earlier var filePath = Path.of(mainLocation.getFilePath()); var creationDate = Instant.ofEpochMilli(liteTaintIssueFromWs.getCreationDate()); ServerTaintIssue taintIssue; var severity = IssueSeverity.valueOf(liteTaintIssueFromWs.getSeverity().name()); var type = RuleType.valueOf(liteTaintIssueFromWs.getType().name()); var ruleDescriptionContextKey = liteTaintIssueFromWs.hasRuleDescriptionContextKey() ? liteTaintIssueFromWs.getRuleDescriptionContextKey() : null; var cleanCodeAttribute = parseProtoCleanCodeAttribute(liteTaintIssueFromWs); var impacts = liteTaintIssueFromWs.getImpactsList().stream() .map(i -> Map.entry( parseProtoSoftwareQuality(i), parseProtoImpactSeverity(i))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); var flows = liteTaintIssueFromWs.getFlowsList().stream().map(TaintIssueDownloader::convertFlows).toList(); if (mainLocation.hasTextRange()) { taintIssue = new ServerTaintIssue(UUID.randomUUID(), liteTaintIssueFromWs.getKey(), liteTaintIssueFromWs.getResolved(), null, liteTaintIssueFromWs.getRuleKey(), mainLocation.getMessage(), filePath, creationDate, severity, type, toServerTaintIssueTextRange(mainLocation.getTextRange()), ruleDescriptionContextKey, cleanCodeAttribute, impacts, flows); } else { taintIssue = new ServerTaintIssue(UUID.randomUUID(), liteTaintIssueFromWs.getKey(), liteTaintIssueFromWs.getResolved(), null, liteTaintIssueFromWs.getRuleKey(), mainLocation.getMessage(), filePath, creationDate, severity, type, null, ruleDescriptionContextKey, cleanCodeAttribute, impacts, flows); } return taintIssue; } private static ServerTaintIssue.Flow convertFlows(Issues.Flow flowFromWs) { return new ServerTaintIssue.Flow(flowFromWs.getLocationsList().stream().map(locationFromWs -> { var filePath = locationFromWs.hasFilePath() ? Path.of(locationFromWs.getFilePath()) : null; if (locationFromWs.hasTextRange()) { return new ServerTaintIssue.ServerIssueLocation(filePath, toServerTaintIssueTextRange(locationFromWs.getTextRange()), locationFromWs.getMessage()); } else { return new ServerTaintIssue.ServerIssueLocation(filePath, null, locationFromWs.getMessage()); } }).toList()); } private static ServerTaintIssue.ServerIssueLocation convertPrimaryLocation(SourceApi sourceApi, Issue issueFromWs, Map componentPathsByKey, Map sourceCodeByKey, SonarLintCancelMonitor cancelMonitor) { var componentPath = componentPathsByKey.get(issueFromWs.getComponent()); if (issueFromWs.hasTextRange()) { var codeSnippet = getCodeSnippet(sourceApi, issueFromWs.getComponent(), issueFromWs.getTextRange(), sourceCodeByKey, cancelMonitor); String textRangeHash; if (codeSnippet != null) { textRangeHash = hash(codeSnippet); } else { // Use empty String, the client will detect a mismatch with real hash and apply UX for mismatched locations textRangeHash = ""; } return new ServerTaintIssue.ServerIssueLocation(componentPath, convertTextRangeFromWs(issueFromWs.getTextRange(), textRangeHash), issueFromWs.getMessage()); } return new ServerTaintIssue.ServerIssueLocation(componentPath, null, issueFromWs.getMessage()); } static String hash(String codeSnippet) { String codeSnippetWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(codeSnippet).replaceAll(""); return DigestUtils.md5Hex(codeSnippetWithoutWhitespaces); } private static TextRangeWithHash convertTextRangeFromWs(TextRange textRange, String hash) { return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartOffset(), textRange.getEndLine(), textRange.getEndOffset(), hash); } @CheckForNull private static String getCodeSnippet(SourceApi sourceApi, String fileKey, TextRange textRange, Map sourceCodeByKey, SonarLintCancelMonitor cancelMonitor) { var sourceCode = getOrFetchSourceCode(sourceApi, fileKey, sourceCodeByKey, cancelMonitor); if (StringUtils.isEmpty(sourceCode)) { return null; } try { return ServerApiUtils.extractCodeSnippet(sourceCode, textRange); } catch (Exception e) { LOG.debug("Unable to compute code snippet of '" + fileKey + "' for text range: " + textRange, e); } return null; } private static String getOrFetchSourceCode(SourceApi sourceApi, String fileKey, Map sourceCodeByKey, SonarLintCancelMonitor cancelMonitor) { return sourceCodeByKey.computeIfAbsent(fileKey, k -> sourceApi .getRawSourceCode(fileKey, cancelMonitor) .orElse("")); } public static class PullTaintResult { private final Instant queryTimestamp; private final List changedIssues; private final Set closedIssueKeys; public PullTaintResult(Instant queryTimestamp, List changedIssues, Set closedIssueKeys) { this.queryTimestamp = queryTimestamp; this.changedIssues = changedIssues; this.closedIssueKeys = closedIssueKeys; } public Instant getQueryTimestamp() { return queryTimestamp; } public List getChangedTaintIssues() { return changedIssues; } public Set getClosedIssueKeys() { return closedIssueKeys; } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/UserSynchronizer.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; public class UserSynchronizer { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ConnectionStorage storage; public UserSynchronizer(ConnectionStorage storage) { this.storage = storage; } /** * Fetches and stores the user id from the server. * Available on SonarQube Cloud and SonarQube Server 2025.6+. */ public void synchronize(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { try { var userId = serverApi.users().getCurrentUserId(cancelMonitor); if (userId != null && !userId.trim().isEmpty()) { storage.user().store(userId.trim()); } } catch (Exception e) { LOG.warn("Failed to synchronize user id from server: {}", e.getMessage()); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/VersionUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.sonarsource.sonarlint.core.commons.Version; public class VersionUtils { private static final Version CURRENT_LTS = Version.create("9.9"); private static final Version MINIMAL_SUPPORTED_VERSION = Version.create("9.9"); private VersionUtils() { } /** * Right now since minimal supported version is equal to current LTS (9.9) this method will always return false. * But it's important to keep it for the future when next LTS will be released, and we will have a grace period again. */ public static boolean isVersionSupportedDuringGracePeriod(Version currentVersion) { return currentVersion.compareTo(CURRENT_LTS) < 0 && currentVersion.compareToIgnoreQualifier(MINIMAL_SUPPORTED_VERSION) >= 0; } public static Version getCurrentLts() { return CURRENT_LTS; } public static Version getMinimalSupportedVersion() { return MINIMAL_SUPPORTED_VERSION; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFix.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import java.util.Arrays; import java.util.Collection; import java.util.Objects; /** * Entity representing AI CodeFix settings persisted in the local storage (H2). * This mirrors org.sonarsource.sonarlint.core.serverconnection.AiCodeFixSettings but * lives in commons to avoid cross-module dependencies. */ public record AiCodeFix( String connectionId, String[] supportedRules, boolean organizationEligible, Enablement enablement, String[] enabledProjectKeys ) { public AiCodeFix(String connectionId, Collection supportedRules, boolean organizationEligible, Enablement enablement, Collection enabledProjectKeys) { this(connectionId, supportedRules.toArray(String[]::new), organizationEligible, enablement, enabledProjectKeys.toArray(String[]::new)); } public enum Enablement { DISABLED, ENABLED_FOR_ALL_PROJECTS, ENABLED_FOR_SOME_PROJECTS } public AiCodeFix { Objects.requireNonNull(connectionId, "connectionId"); Objects.requireNonNull(supportedRules, "supportedRules"); Objects.requireNonNull(enablement, "enablement"); Objects.requireNonNull(enabledProjectKeys, "enabledProjectKeys"); } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; var aiCodeFix = (AiCodeFix) o; return organizationEligible == aiCodeFix.organizationEligible && Objects.equals(connectionId, aiCodeFix.connectionId) && enablement == aiCodeFix.enablement && Objects.deepEquals(supportedRules, aiCodeFix.supportedRules) && Objects.deepEquals(enabledProjectKeys, aiCodeFix.enabledProjectKeys); } @Override public int hashCode() { return Objects.hash(connectionId, Arrays.hashCode(supportedRules), organizationEligible, enablement, Arrays.hashCode(enabledProjectKeys)); } @Override public String toString() { return "AiCodeFix{" + "connectionId='" + connectionId + '\'' + ", supportedRules=" + Arrays.toString(supportedRules) + ", organizationEligible=" + organizationEligible + ", enablement=" + enablement + ", enabledProjectKeys=" + Arrays.toString(enabledProjectKeys) + '}'; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFixFeatureEnablement.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; public enum AiCodeFixFeatureEnablement { DISABLED, ENABLED_FOR_ALL_PROJECTS, ENABLED_FOR_SOME_PROJECTS } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFixRepository.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import java.util.Optional; import java.util.Set; import org.jooq.DSLContext; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.AI_CODEFIX_SETTINGS; /** * Repository for persisting and retrieving AiCodeFix entity using the local H2 database. * Settings are stored per server connection, addressed by connectionId. */ public class AiCodeFixRepository { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final DSLContext database; public AiCodeFixRepository(DSLContext dslContext) { this.database = dslContext; } public Optional get(String connectionId) { var rec = database .select(AI_CODEFIX_SETTINGS.SUPPORTED_RULES, AI_CODEFIX_SETTINGS.ORGANIZATION_ELIGIBLE, AI_CODEFIX_SETTINGS.ENABLEMENT, AI_CODEFIX_SETTINGS.ENABLED_PROJECT_KEYS) .from(AI_CODEFIX_SETTINGS) .where(AI_CODEFIX_SETTINGS.CONNECTION_ID.eq(connectionId)) .fetchOne(); if (rec == null) { return Optional.empty(); } var supportedRules = rec.get(AI_CODEFIX_SETTINGS.SUPPORTED_RULES); var organizationEligible = Boolean.TRUE.equals(rec.get(AI_CODEFIX_SETTINGS.ORGANIZATION_ELIGIBLE)); var enablement = AiCodeFix.Enablement.valueOf(rec.get(AI_CODEFIX_SETTINGS.ENABLEMENT)); var enabledProjectKeys = rec.get(AI_CODEFIX_SETTINGS.ENABLED_PROJECT_KEYS); return Optional.of(new AiCodeFix(connectionId, supportedRules, organizationEligible, enablement, enabledProjectKeys)); } public void upsert(AiCodeFix entity) { database .insertInto(AI_CODEFIX_SETTINGS, AI_CODEFIX_SETTINGS.CONNECTION_ID, AI_CODEFIX_SETTINGS.SUPPORTED_RULES, AI_CODEFIX_SETTINGS.ORGANIZATION_ELIGIBLE, AI_CODEFIX_SETTINGS.ENABLEMENT, AI_CODEFIX_SETTINGS.ENABLED_PROJECT_KEYS) .values(entity.connectionId(), entity.supportedRules(), entity.organizationEligible(), entity.enablement().name(), entity.enabledProjectKeys()) .onDuplicateKeyUpdate() .set(AI_CODEFIX_SETTINGS.SUPPORTED_RULES, entity.supportedRules()) .set(AI_CODEFIX_SETTINGS.ORGANIZATION_ELIGIBLE, entity.organizationEligible()) .set(AI_CODEFIX_SETTINGS.ENABLEMENT, entity.enablement().name()) .set(AI_CODEFIX_SETTINGS.ENABLED_PROJECT_KEYS, entity.enabledProjectKeys()) .execute(); } public void deleteUnknownConnections(Set knownConnectionIds) { database.dsl().deleteFrom(AI_CODEFIX_SETTINGS) .where(AI_CODEFIX_SETTINGS.CONNECTION_ID.notIn(knownConnectionIds)) .execute(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFixSettings.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import java.util.Set; public record AiCodeFixSettings(Set supportedRules, boolean isOrganizationEligible, AiCodeFixFeatureEnablement enablement, Set enabledProjectKeys) { public boolean isFeatureEnabled(String projectKey) { return isOrganizationEligible && (enablement.equals(AiCodeFixFeatureEnablement.ENABLED_FOR_ALL_PROJECTS) || (enablement.equals(AiCodeFixFeatureEnablement.ENABLED_FOR_SOME_PROJECTS) && enabledProjectKeys.contains(projectKey))); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFixSettingsSynchronizer.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.component.ServerProject; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import org.sonarsource.sonarlint.core.serverapi.organization.ServerOrganization; import org.sonarsource.sonarlint.core.serverconnection.ConnectionStorage; import org.sonarsource.sonarlint.core.serverconnection.OrganizationSynchronizer; public class AiCodeFixSettingsSynchronizer { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final Version MIN_SQS_VERSION_SUPPORTING_AI_CODEFIX = Version.create("2025.3"); private final ConnectionStorage storage; private final OrganizationSynchronizer organizationSynchronizer; private final AiCodeFixRepository aiCodeFixRepository; public AiCodeFixSettingsSynchronizer(ConnectionStorage storage, OrganizationSynchronizer organizationSynchronizer, AiCodeFixRepository aiCodeFixRepository) { this.storage = storage; this.organizationSynchronizer = organizationSynchronizer; this.aiCodeFixRepository = aiCodeFixRepository; } public void synchronize(ServerApi serverApi, Version serverVersion, Set projectKeys, SonarLintCancelMonitor cancelMonitor) { if (serverApi.isSonarCloud()) { synchronizeForSonarQubeCloud(serverApi, cancelMonitor); } else { synchronizeForSonarQubeServer(serverApi, serverVersion, projectKeys, cancelMonitor); } } private void synchronizeForSonarQubeCloud(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) { var userOrganizations = serverApi.isSonarCloud() ? serverApi.organization().listUserOrganizations(cancelMonitor) : List.of(); if (userBelongsToOrganization(serverApi, userOrganizations)) { try { var supportedRules = serverApi.fixSuggestions().getSupportedRules(cancelMonitor); var organization = organizationSynchronizer.readOrSynchronizeOrganization(serverApi, cancelMonitor); var organizationConfig = serverApi.fixSuggestions().getOrganizationConfigs(organization.id(), cancelMonitor); var aiCodeFixConfiguration = organizationConfig.aiCodeFix(); var enabledProjectKeys = aiCodeFixConfiguration.enabledProjectKeys(); var enabled = enabledProjectKeys == null ? Set.of() : enabledProjectKeys; var entity = new AiCodeFix( storage.connectionId(), supportedRules.rules(), aiCodeFixConfiguration.organizationEligible(), AiCodeFix.Enablement.valueOf(aiCodeFixConfiguration.enablement().name()), enabled); aiCodeFixRepository.upsert(entity); } catch (Exception e) { LOG.error("Error synchronizing AI CodeFix settings for SonarQube Cloud", e); } } } private void synchronizeForSonarQubeServer(ServerApi serverApi, Version serverVersion, Set projectKeys, SonarLintCancelMonitor cancelMonitor) { try { if (serverVersion.satisfiesMinRequirement(MIN_SQS_VERSION_SUPPORTING_AI_CODEFIX) && serverApi.features().list(cancelMonitor).contains(Feature.AI_CODE_FIX)) { var supportedRules = serverApi.fixSuggestions().getSupportedRules(cancelMonitor); var enabledProjectKeys = projectKeys.stream() .filter(projectKey -> serverApi.component().getProject(projectKey, cancelMonitor).filter(ServerProject::isAiCodeFixEnabled).isPresent()).collect(Collectors.toSet()); var entity = new AiCodeFix( storage.connectionId(), supportedRules.rules(), true, AiCodeFix.Enablement.ENABLED_FOR_SOME_PROJECTS, enabledProjectKeys); aiCodeFixRepository.upsert(entity); } } catch (Exception e) { LOG.error("Error synchronizing AI CodeFix settings for SonarQube Server", e); } } private static boolean userBelongsToOrganization(ServerApi serverApi, List userOrganizations) { return serverApi.getOrganizationKey().filter(orgKey -> userOrganizations.stream().anyMatch(org -> org.getKey().equals(orgKey))).isPresent(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/package-info.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/FileLevelServerIssue.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; /** * Issues reported at file level. */ public class FileLevelServerIssue extends ServerIssue { public FileLevelServerIssue(@Nullable UUID id, String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, Map impacts) { super(id, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, impacts); } /** * constructor for backward compatibility, after finalization of migration from Xodus to H2 should not be used * when using with H2 UUID should always be set */ public FileLevelServerIssue(String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, Map impacts) { this(null, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, impacts); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/Findings.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.util.ArrayList; import java.util.List; import org.sonarsource.sonarlint.core.commons.KnownFinding; public record Findings(List issues, List hotspots) { public Findings mergeWith(Findings other) { var mergedIssues = new ArrayList<>(issues); mergedIssues.addAll(other.issues); var mergedHotspots = new ArrayList(hotspots); mergedHotspots.addAll(other.hotspots); return new Findings(mergedIssues, mergedHotspots); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/KnownFindingsRepository.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.jooq.Configuration; import org.jooq.Record; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.KnownFindingType; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.commons.storage.model.Tables; import org.sonarsource.sonarlint.core.commons.storage.model.tables.records.KnownFindingsRecord; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.KNOWN_FINDINGS; public class KnownFindingsRepository { private final SonarLintDatabase database; public KnownFindingsRepository(SonarLintDatabase database) { this.database = database; } public void storeFindings(Map> findingsPerFilePerConfigScopeId) { var records = findingsPerFilePerConfigScopeId.entrySet().stream() .flatMap(KnownFindingsRepository::expandConfigScope) .toList(); database.dsl().deleteFrom(Tables.KNOWN_FINDINGS).execute(); database.dsl().batchInsert(records).execute(); } private static Stream expandConfigScope(Map.Entry> configScopeEntry) { var configScopeId = configScopeEntry.getKey(); return configScopeEntry.getValue().entrySet().stream() .flatMap(fileEntry -> expandFileFindings(configScopeId, fileEntry)); } private static Stream expandFileFindings(String configScopeId, Map.Entry fileEntry) { var filePath = fileEntry.getKey(); var findings = fileEntry.getValue(); return Stream.concat( findings.issues().stream() .map(f -> createRecord(f, configScopeId, filePath, KnownFindingType.ISSUE)), findings.hotspots().stream() .map(f -> createRecord(f, configScopeId, filePath, KnownFindingType.HOTSPOT)) ); } private static KnownFindingsRecord createRecord(KnownFinding finding, String configScopeId, Path filePath, KnownFindingType type) { var textRangeWithHash = finding.getTextRangeWithHash(); var lineWithHash = finding.getLineWithHash(); var introductionDate = LocalDateTime.ofInstant(finding.getIntroductionDate(), ZoneOffset.UTC); return new KnownFindingsRecord( finding.getId(), configScopeId, filePath.toString(), finding.getServerKey(), finding.getRuleKey(), finding.getMessage(), introductionDate, type.name(), textRangeWithHash == null ? null : textRangeWithHash.getStartLine(), textRangeWithHash == null ? null : textRangeWithHash.getStartLineOffset(), textRangeWithHash == null ? null : textRangeWithHash.getEndLine(), textRangeWithHash == null ? null : textRangeWithHash.getEndLineOffset(), textRangeWithHash == null ? null : textRangeWithHash.getHash(), lineWithHash == null ? null : lineWithHash.getNumber(), lineWithHash == null ? null : lineWithHash.getHash()); } public void storeKnownIssues(String configurationScopeId, Path clientRelativePath, List newKnownIssues) { storeKnownFindings(configurationScopeId, clientRelativePath, newKnownIssues, KnownFindingType.ISSUE); } public void storeKnownSecurityHotspots(String configurationScopeId, Path clientRelativePath, List newKnownSecurityHotspots) { storeKnownFindings(configurationScopeId, clientRelativePath, newKnownSecurityHotspots, KnownFindingType.HOTSPOT); } public List loadSecurityHotspotsForFile(String configurationScopeId, Path filePath) { return getKnownFindingsForFile(configurationScopeId, filePath, KnownFindingType.HOTSPOT); } public List loadIssuesForFile(String configurationScopeId, Path filePath) { return getKnownFindingsForFile(configurationScopeId, filePath, KnownFindingType.ISSUE); } private void storeKnownFindings(String configurationScopeId, Path clientRelativePath, List newKnownFindings, KnownFindingType type) { database.dsl().transaction((Configuration trx) -> newKnownFindings.forEach(finding -> { var textRangeWithHash = finding.getTextRangeWithHash(); var startLine = textRangeWithHash == null ? null : textRangeWithHash.getStartLine(); var startLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getStartLineOffset(); var endLine = textRangeWithHash == null ? null : textRangeWithHash.getEndLine(); var endLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getEndLineOffset(); var textRangeHash = textRangeWithHash == null ? null : textRangeWithHash.getHash(); var lineWithHash = finding.getLineWithHash(); var line = lineWithHash == null ? null : lineWithHash.getNumber(); var lineHash = lineWithHash == null ? null : lineWithHash.getHash(); var introDate = LocalDateTime.ofInstant(finding.getIntroductionDate(), ZoneOffset.UTC); trx.dsl().mergeInto(KNOWN_FINDINGS) .using(trx.dsl().selectOne()) .on(KNOWN_FINDINGS.ID.eq(finding.getId())) .whenMatchedThenUpdate() .set(KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID, configurationScopeId) .set(KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH, clientRelativePath.toString()) .set(KNOWN_FINDINGS.SERVER_KEY, finding.getServerKey()) .set(KNOWN_FINDINGS.RULE_KEY, finding.getRuleKey()) .set(KNOWN_FINDINGS.MESSAGE, finding.getMessage()) .set(KNOWN_FINDINGS.INTRODUCTION_DATE, introDate) .set(KNOWN_FINDINGS.FINDING_TYPE, type.name()) .set(KNOWN_FINDINGS.START_LINE, startLine) .set(KNOWN_FINDINGS.START_LINE_OFFSET, startLineOffset) .set(KNOWN_FINDINGS.END_LINE, endLine) .set(KNOWN_FINDINGS.END_LINE_OFFSET, endLineOffset) .set(KNOWN_FINDINGS.TEXT_RANGE_HASH, textRangeHash) .set(KNOWN_FINDINGS.LINE, line) .set(KNOWN_FINDINGS.LINE_HASH, lineHash) .whenNotMatchedThenInsert(KNOWN_FINDINGS.ID, KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID, KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH, KNOWN_FINDINGS.SERVER_KEY, KNOWN_FINDINGS.RULE_KEY, KNOWN_FINDINGS.MESSAGE, KNOWN_FINDINGS.INTRODUCTION_DATE, KNOWN_FINDINGS.FINDING_TYPE, KNOWN_FINDINGS.START_LINE, KNOWN_FINDINGS.START_LINE_OFFSET, KNOWN_FINDINGS.END_LINE, KNOWN_FINDINGS.END_LINE_OFFSET, KNOWN_FINDINGS.TEXT_RANGE_HASH, KNOWN_FINDINGS.LINE, KNOWN_FINDINGS.LINE_HASH) .values(finding.getId(), configurationScopeId, clientRelativePath.toString(), finding.getServerKey(), finding.getRuleKey(), finding.getMessage(), introDate, type.name(), startLine, startLineOffset, endLine, endLineOffset, textRangeHash, line, lineHash) .execute(); })); } private List getKnownFindingsForFile(String configurationScopeId, Path filePath, KnownFindingType type) { var issuesInFile = database.dsl() .selectFrom(KNOWN_FINDINGS) .where(KNOWN_FINDINGS.CONFIGURATION_SCOPE_ID.eq(configurationScopeId) .and(KNOWN_FINDINGS.IDE_RELATIVE_FILE_PATH.eq(filePath.toString())) .and(KNOWN_FINDINGS.FINDING_TYPE.eq(type.name()))) .fetch(); return issuesInFile.stream() .map(KnownFindingsRepository::recordToKnownFinding) .toList(); } private static KnownFinding recordToKnownFinding(Record rec) { var id = rec.get(KNOWN_FINDINGS.ID); var introductionDate = rec.get(KNOWN_FINDINGS.INTRODUCTION_DATE).toInstant(ZoneOffset.UTC); var textRangeWithHash = getTextRangeWithHash(rec); var lineWithHash = getLineWithHash(rec); return new KnownFinding( id, rec.get(KNOWN_FINDINGS.SERVER_KEY), textRangeWithHash, lineWithHash, rec.get(KNOWN_FINDINGS.RULE_KEY), rec.get(KNOWN_FINDINGS.MESSAGE), introductionDate); } private static LineWithHash getLineWithHash(Record rec) { var line = rec.get(KNOWN_FINDINGS.LINE); if (line == null) { return null; } var hash = rec.get(KNOWN_FINDINGS.LINE_HASH); return new LineWithHash(line, hash); } private static TextRangeWithHash getTextRangeWithHash(Record rec) { var startLine = rec.get(KNOWN_FINDINGS.START_LINE); if (startLine == null) { return null; } var endLine = rec.get(KNOWN_FINDINGS.END_LINE); var startLineOffset = rec.get(KNOWN_FINDINGS.START_LINE_OFFSET); var endLineOffset = rec.get(KNOWN_FINDINGS.END_LINE_OFFSET); var hash = rec.get(KNOWN_FINDINGS.TEXT_RANGE_HASH); return new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, hash); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/LineLevelServerIssue.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; /** * Issues with line level precision (from old /batch/issues WS, in SQ < 9.6 and SC) */ public class LineLevelServerIssue extends ServerIssue { private int line; private String lineHash; public LineLevelServerIssue(@Nullable UUID id, String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, String lineHash, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, int line, Map impacts) { super(id, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, impacts); this.lineHash = lineHash; this.line = line; } /** * constructor for backward compatibility, after finalization of migration from Xodus to H2 should not be used * when using with H2 UUID should always be set */ public LineLevelServerIssue(String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, String lineHash, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, int line, Map impacts) { this(null, key, resolved, resolutionStatus, ruleKey, message, lineHash, filePath, creationDate, userSeverity, type, line, impacts); } public String getLineHash() { return lineHash; } public Integer getLine() { return line; } public LineLevelServerIssue setLineHash(String lineHash) { this.lineHash = lineHash; return this; } public LineLevelServerIssue setLine(int line) { this.line = line; return this; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/LocalOnlyIssuesRepository.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import org.jooq.Configuration; import org.jooq.DSLContext; import org.jooq.Record; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssueResolution; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.storage.model.tables.records.LocalOnlyIssuesRecord; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.LOCAL_ONLY_ISSUES; public class LocalOnlyIssuesRepository { private final DSLContext database; public LocalOnlyIssuesRepository(DSLContext database) { this.database = database; } public List loadForFile(String configurationScopeId, Path filePath) { var issuesInFile = database .selectFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.CONFIGURATION_SCOPE_ID.eq(configurationScopeId) .and(LOCAL_ONLY_ISSUES.SERVER_RELATIVE_PATH.eq(filePath.toString()))) .fetch(); return issuesInFile.stream() .map(LocalOnlyIssuesRepository::recordToLocalOnlyIssue) .toList(); } public List loadAll(String configurationScopeId) { var allIssues = database .selectFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.CONFIGURATION_SCOPE_ID.eq(configurationScopeId)) .fetch(); return allIssues.stream() .map(LocalOnlyIssuesRepository::recordToLocalOnlyIssue) .toList(); } public void storeIssues(Map> issuesPerConfigScopeId) { database.deleteFrom(LOCAL_ONLY_ISSUES).execute(); database.batchInsert(issuesPerConfigScopeId.entrySet().stream() .flatMap(entry -> { var configScopeId = entry.getKey(); return entry.getValue().stream().map( issue -> { var resolution = issue.getResolution(); var textRangeWithHash = issue.getTextRangeWithHash(); var lineWithHash = issue.getLineWithHash(); return new LocalOnlyIssuesRecord( issue.getId(), configScopeId, issue.getServerRelativePath().toString(), issue.getRuleKey(), issue.getMessage(), resolution == null ? null : resolution.getStatus().name(), resolution == null ? null : LocalDateTime.ofInstant(resolution.getResolutionDate(), ZoneOffset.UTC), resolution == null ? null : resolution.getComment(), textRangeWithHash == null ? null : textRangeWithHash.getStartLine(), textRangeWithHash == null ? null : textRangeWithHash.getStartLineOffset(), textRangeWithHash == null ? null : textRangeWithHash.getEndLine(), textRangeWithHash == null ? null : textRangeWithHash.getEndLineOffset(), textRangeWithHash == null ? null : textRangeWithHash.getHash(), lineWithHash == null ? null : lineWithHash.getNumber(), lineWithHash == null ? null : lineWithHash.getHash()); }); }) .toList()) .execute(); } public void storeLocalOnlyIssue(String configurationScopeId, LocalOnlyIssue issue) { database.transaction((Configuration trx) -> { var textRangeWithHash = issue.getTextRangeWithHash(); var startLine = textRangeWithHash == null ? null : textRangeWithHash.getStartLine(); var startLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getStartLineOffset(); var endLine = textRangeWithHash == null ? null : textRangeWithHash.getEndLine(); var endLineOffset = textRangeWithHash == null ? null : textRangeWithHash.getEndLineOffset(); var textRangeHash = textRangeWithHash == null ? null : textRangeWithHash.getHash(); var lineWithHash = issue.getLineWithHash(); var line = lineWithHash == null ? null : lineWithHash.getNumber(); var lineHash = lineWithHash == null ? null : lineWithHash.getHash(); var resolution = issue.getResolution(); var resolutionStatus = resolution == null ? null : resolution.getStatus().name(); var resolutionDate = resolution == null ? null : LocalDateTime.ofInstant(resolution.getResolutionDate(), ZoneOffset.UTC); var comment = resolution == null ? null : resolution.getComment(); trx.dsl().mergeInto(LOCAL_ONLY_ISSUES) .using(trx.dsl().selectOne()) .on(LOCAL_ONLY_ISSUES.ID.eq(issue.getId())) .whenMatchedThenUpdate() .set(LOCAL_ONLY_ISSUES.CONFIGURATION_SCOPE_ID, configurationScopeId) .set(LOCAL_ONLY_ISSUES.SERVER_RELATIVE_PATH, issue.getServerRelativePath().toString()) .set(LOCAL_ONLY_ISSUES.RULE_KEY, issue.getRuleKey()) .set(LOCAL_ONLY_ISSUES.MESSAGE, issue.getMessage()) .set(LOCAL_ONLY_ISSUES.RESOLUTION_STATUS, resolutionStatus) .set(LOCAL_ONLY_ISSUES.RESOLUTION_DATE, resolutionDate) .set(LOCAL_ONLY_ISSUES.COMMENT, comment) .set(LOCAL_ONLY_ISSUES.START_LINE, startLine) .set(LOCAL_ONLY_ISSUES.START_LINE_OFFSET, startLineOffset) .set(LOCAL_ONLY_ISSUES.END_LINE, endLine) .set(LOCAL_ONLY_ISSUES.END_LINE_OFFSET, endLineOffset) .set(LOCAL_ONLY_ISSUES.TEXT_RANGE_HASH, textRangeHash) .set(LOCAL_ONLY_ISSUES.LINE, line) .set(LOCAL_ONLY_ISSUES.LINE_HASH, lineHash) .whenNotMatchedThenInsert( LOCAL_ONLY_ISSUES.ID, LOCAL_ONLY_ISSUES.CONFIGURATION_SCOPE_ID, LOCAL_ONLY_ISSUES.SERVER_RELATIVE_PATH, LOCAL_ONLY_ISSUES.RULE_KEY, LOCAL_ONLY_ISSUES.MESSAGE, LOCAL_ONLY_ISSUES.RESOLUTION_STATUS, LOCAL_ONLY_ISSUES.RESOLUTION_DATE, LOCAL_ONLY_ISSUES.COMMENT, LOCAL_ONLY_ISSUES.START_LINE, LOCAL_ONLY_ISSUES.START_LINE_OFFSET, LOCAL_ONLY_ISSUES.END_LINE, LOCAL_ONLY_ISSUES.END_LINE_OFFSET, LOCAL_ONLY_ISSUES.TEXT_RANGE_HASH, LOCAL_ONLY_ISSUES.LINE, LOCAL_ONLY_ISSUES.LINE_HASH) .values( issue.getId(), configurationScopeId, issue.getServerRelativePath().toString(), issue.getRuleKey(), issue.getMessage(), resolutionStatus, resolutionDate, comment, startLine, startLineOffset, endLine, endLineOffset, textRangeHash, line, lineHash) .execute(); }); } public boolean removeIssue(UUID issueId) { var deleted = database .deleteFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.ID.eq(issueId)) .execute(); return deleted > 0; } public boolean removeAllIssuesForFile(String configurationScopeId, Path filePath) { var deleted = database .deleteFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.CONFIGURATION_SCOPE_ID.eq(configurationScopeId) .and(LOCAL_ONLY_ISSUES.SERVER_RELATIVE_PATH.eq(filePath.toString()))) .execute(); return deleted > 0; } public Optional find(UUID issueId) { var issue = database .selectFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.ID.eq(issueId)) .fetchOne(); return issue == null ? Optional.empty() : Optional.of(recordToLocalOnlyIssue(issue)); } public void purgeIssuesOlderThan(Instant limit) { var limitDateTime = LocalDateTime.ofInstant(limit, ZoneOffset.UTC); database .deleteFrom(LOCAL_ONLY_ISSUES) .where(LOCAL_ONLY_ISSUES.RESOLUTION_DATE.isNotNull() .and(LOCAL_ONLY_ISSUES.RESOLUTION_DATE.le(limitDateTime))) .execute(); } private static LocalOnlyIssue recordToLocalOnlyIssue(Record rec) { var id = rec.get(LOCAL_ONLY_ISSUES.ID); var serverRelativePath = Path.of(rec.get(LOCAL_ONLY_ISSUES.SERVER_RELATIVE_PATH)); var ruleKey = rec.get(LOCAL_ONLY_ISSUES.RULE_KEY); var message = rec.get(LOCAL_ONLY_ISSUES.MESSAGE); var textRangeWithHash = getTextRangeWithHash(rec); var lineWithHash = getLineWithHash(rec); LocalOnlyIssueResolution resolution = null; var resolutionStatus = rec.get(LOCAL_ONLY_ISSUES.RESOLUTION_STATUS); var resolutionDate = rec.get(LOCAL_ONLY_ISSUES.RESOLUTION_DATE); if (resolutionStatus != null && resolutionDate != null) { var status = IssueStatus.valueOf(resolutionStatus); var instant = resolutionDate.toInstant(ZoneOffset.UTC); var comment = rec.get(LOCAL_ONLY_ISSUES.COMMENT); resolution = new LocalOnlyIssueResolution(status, instant, comment); } return new LocalOnlyIssue(id, serverRelativePath, textRangeWithHash, lineWithHash, ruleKey, message, resolution); } private static LineWithHash getLineWithHash(Record rec) { var line = rec.get(LOCAL_ONLY_ISSUES.LINE); if (line == null) { return null; } var hash = rec.get(LOCAL_ONLY_ISSUES.LINE_HASH); return new LineWithHash(line, hash); } private static TextRangeWithHash getTextRangeWithHash(Record rec) { var startLine = rec.get(LOCAL_ONLY_ISSUES.START_LINE); if (startLine == null) { return null; } var endLine = rec.get(LOCAL_ONLY_ISSUES.END_LINE); var startLineOffset = rec.get(LOCAL_ONLY_ISSUES.START_LINE_OFFSET); var endLineOffset = rec.get(LOCAL_ONLY_ISSUES.END_LINE_OFFSET); var hash = rec.get(LOCAL_ONLY_ISSUES.TEXT_RANGE_HASH); if (hash == null) { return null; } return new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, hash); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/RangeLevelServerIssue.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.Map; import java.util.UUID; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; /** * Issues with precise location (from api/issues/pull, SQ >= 9.6) */ public class RangeLevelServerIssue extends ServerIssue { private TextRangeWithHash textRange; public RangeLevelServerIssue(@Nullable UUID id, String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, TextRangeWithHash textRange, Map impacts) { super(id, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, impacts); this.textRange = textRange; } /** * constructor for backward compatibility, after finalization of migration from Xodus to H2 should not be used * when using with H2 UUID should always be set */ public RangeLevelServerIssue(String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, TextRangeWithHash textRange, Map impacts) { this(null, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, textRange, impacts); } public TextRangeWithHash getTextRange() { return textRange; } public RangeLevelServerIssue setTextRange(TextRangeWithHash textRange) { this.textRange = textRange; return this; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/ServerDependencyRisk.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; public record ServerDependencyRisk(UUID key, Type type, Severity severity, SoftwareQuality quality, Status status, String packageName, String packageVersion, @Nullable String vulnerabilityId, @Nullable String cvssScore, List transitions) { public ServerDependencyRisk withStatus(Status newStatus) { var newTransitions = new ArrayList<>(Arrays.asList(Transition.values())); newTransitions.remove(Transition.FIXED); newTransitions.remove(newStatus.equals(Status.OPEN) ? Transition.REOPEN : Transition.valueOf(newStatus.name())); return new ServerDependencyRisk(key, type, severity, quality, newStatus, packageName, packageVersion, vulnerabilityId, cvssScore, newTransitions); } public enum Severity { INFO, LOW, MEDIUM, HIGH, BLOCKER } public enum SoftwareQuality { MAINTAINABILITY, RELIABILITY, SECURITY } public enum Type { VULNERABILITY, PROHIBITED_LICENSE } public enum Status { OPEN, CONFIRM, ACCEPT, SAFE, FIXED } public enum Transition { CONFIRM, REOPEN, SAFE, FIXED, ACCEPT } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/ServerFinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; public interface ServerFinding { String getRuleKey(); } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/ServerIssue.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.Map; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; public abstract class ServerIssue> implements ServerFinding { private UUID id; private String key; private boolean resolved; private IssueStatus resolutionStatus; private String ruleKey; private String message; private Path filePath; private Instant creationDate; private IssueSeverity userSeverity; private RuleType type; private Map impacts; protected ServerIssue(@Nullable UUID id, String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, @Nullable IssueSeverity userSeverity, RuleType type, Map impacts) { this.id = id; this.key = key; this.resolved = resolved; this.resolutionStatus = resolutionStatus; this.ruleKey = ruleKey; this.message = message; this.filePath = filePath; this.creationDate = creationDate; this.userSeverity = userSeverity; this.type = type; this.impacts = impacts; } @CheckForNull public UUID getId() { return id; } public String getKey() { return key; } public boolean isResolved() { return resolved; } @CheckForNull public IssueStatus getResolutionStatus() { return resolutionStatus; } @Override public String getRuleKey() { return ruleKey; } public String getMessage() { return message; } public Path getFilePath() { return filePath; } public Instant getCreationDate() { return creationDate; } @CheckForNull public IssueSeverity getUserSeverity() { return userSeverity; } public RuleType getType() { return type; } public Map getImpacts() { return impacts; } public G setId(@Nullable UUID id) { this.id = id; return (G) this; } public G setKey(String key) { this.key = key; return (G) this; } public G setResolutionStatus(@Nullable IssueStatus resolutionStatus) { this.resolutionStatus = resolutionStatus; return (G) this; } public G setRuleKey(String ruleKey) { this.ruleKey = ruleKey; return (G) this; } public G setMessage(String message) { this.message = message; return (G) this; } public G setFilePath(Path filePath) { this.filePath = filePath; return (G) this; } public G setCreationDate(Instant creationDate) { this.creationDate = creationDate; return (G) this; } public G setUserSeverity(@Nullable IssueSeverity userSeverity) { this.userSeverity = userSeverity; return (G) this; } public G setType(RuleType type) { this.type = type; return (G) this; } public G setResolved(boolean resolved) { this.resolved = resolved; return (G) this; } public G setImpacts(Map impacts) { this.impacts = impacts; return (G) this; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/ServerTaintIssue.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; public class ServerTaintIssue implements ServerFinding { private final UUID id; private final String key; private boolean resolved; @Nullable private final IssueStatus resolutionStatus; private final String ruleKey; private final String message; private final Path filePath; private final Instant creationDate; private IssueSeverity severity; private RuleType type; private final List flows; private final TextRangeWithHash textRange; private Map impacts; @Nullable private final String ruleDescriptionContextKey; @Nullable private final CleanCodeAttribute cleanCodeAttribute; public ServerTaintIssue(UUID id, String key, boolean resolved, @Nullable IssueStatus resolutionStatus, String ruleKey, String message, Path filePath, Instant creationDate, IssueSeverity severity, RuleType type, @Nullable TextRangeWithHash textRange, @Nullable String ruleDescriptionContextKey, @Nullable CleanCodeAttribute cleanCodeAttribute, Map impacts, List flows) { this.id = id; this.key = key; this.resolved = resolved; this.resolutionStatus = resolutionStatus; this.ruleKey = ruleKey; this.message = message; this.filePath = filePath; this.creationDate = creationDate; this.severity = severity; this.type = type; this.textRange = textRange; this.ruleDescriptionContextKey = ruleDescriptionContextKey; this.cleanCodeAttribute = cleanCodeAttribute; this.impacts = impacts; this.flows = flows; } public UUID getId() { return id; } public String getSonarServerKey() { return key; } public boolean isResolved() { return resolved; } @CheckForNull public IssueStatus getResolutionStatus() { return resolutionStatus; } @Override public String getRuleKey() { return ruleKey; } public String getMessage() { return message; } public Path getFilePath() { return filePath; } public Instant getCreationDate() { return creationDate; } public IssueSeverity getSeverity() { return severity; } public RuleType getType() { return type; } @CheckForNull public TextRangeWithHash getTextRange() { return textRange; } @CheckForNull public String getRuleDescriptionContextKey() { return ruleDescriptionContextKey; } public List getFlows() { return flows; } public Optional getCleanCodeAttribute() { return Optional.ofNullable(cleanCodeAttribute); } public Map getImpacts() { return impacts; } public ServerTaintIssue setResolved(boolean resolved) { this.resolved = resolved; return this; } public ServerTaintIssue setImpacts(Map impacts) { this.impacts = impacts; return this; } public ServerTaintIssue setSeverity(IssueSeverity severity) { this.severity = severity; return this; } public ServerTaintIssue setType(RuleType type) { this.type = type; return this; } public record Flow(List locations) { } public record ServerIssueLocation(@Nullable Path filePath, @Nullable TextRangeWithHash textRange, @Nullable String message) { } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/issues/package-info.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverconnection.issues; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/package-info.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverconnection; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/prefix/FileTreeMatcher.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.prefix; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import static java.util.Collections.reverseOrder; public class FileTreeMatcher { public Result match(List serverRelativePaths, List ideRelativePaths) { var reversePathTree = new ReversePathTree(); Map resultScores = new LinkedHashMap<>(); // No need to index server files if no ide path ends with the same filename Set ideFilenames = ideRelativePaths.stream().map(Path::getFileName).collect(Collectors.toSet()); serverRelativePaths.stream().filter(sqPath -> ideFilenames.contains(sqPath.getFileName())).forEach(reversePathTree::index); for (Path ide : ideRelativePaths) { var match = reversePathTree.findLongestSuffixMatches(ide); if (match.matchLen() > 0) { var idePrefix = getIdePrefix(ide, match); for (Path sqPrefix : match.matchPrefixes()) { var r = new Result(idePrefix, sqPrefix); resultScores.compute(r, (p, i) -> computeScore(i, match)); } } } return higherScoreResult(resultScores); } private static double computeScore(@Nullable Double currentScore, ReversePathTree.Match match) { var matchScore = (double) match.matchLen() / match.matchPrefixes().size(); return currentScore != null ? (currentScore.doubleValue() + matchScore) : matchScore; } private static Path getIdePrefix(Path idePath, ReversePathTree.Match match) { var prefixLen = depth(idePath) - match.matchLen(); if (prefixLen > 0) { return idePath.subpath(0, depth(idePath) - match.matchLen()); } return Paths.get(""); } private static Result higherScoreResult(Map prefixes) { // Prefere higher score Comparator> c = Comparator.comparing(Map.Entry::getValue); c = c // fallback on prefix depth .thenComparing(x -> depth(x.getKey().serverPrefix), reverseOrder()) // fallback on prefix lexicographic order .thenComparing(x -> x.getKey().serverPrefix.toString(), reverseOrder()); return prefixes.entrySet().stream() .max(c) .map(Map.Entry::getKey) .orElse(new Result(Paths.get(""), Paths.get(""))); } private static int depth(Path path) { return path.toString().isEmpty() ? 0 : path.getNameCount(); } public static class Result { private final Path idePrefix; private final Path serverPrefix; Result(Path idePrefix, Path serverPrefix) { this.idePrefix = idePrefix; this.serverPrefix = serverPrefix; } public Path idePrefix() { return idePrefix; } public Path sqPrefix() { return serverPrefix; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } var result = (Result) o; return Objects.equals(idePrefix, result.idePrefix) && Objects.equals(serverPrefix, result.serverPrefix); } @Override public int hashCode() { return Objects.hash(idePrefix, serverPrefix); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/prefix/ReversePathTree.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.prefix; import java.nio.file.Path; import java.nio.file.Paths; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; class ReversePathTree { private final Node root = new MultipleChildrenNode(); public void index(Path path) { Node parent = null; var currentNode = root; Path currentNodePath = null; for (var i = path.getNameCount() - 1; i >= 0; i--) { var childNodePath = path.getName(i); var result = currentNode.computeChildrenIfAbsent(parent, currentNodePath, childNodePath); parent = result[0]; currentNode = result[1]; currentNodePath = childNodePath; } currentNode.setTerminal(true); } public Match findLongestSuffixMatches(Path path) { var currentNode = root; var matchLen = 0; while (matchLen < path.getNameCount()) { var nextEl = path.getName(path.getNameCount() - matchLen - 1); var nextNode = currentNode.getChild(nextEl); if (nextNode == null) { break; } matchLen++; currentNode = nextNode; } return collectAllPrefixes(currentNode, matchLen); } private static Match collectAllPrefixes(Node node, int matchLen) { List paths = new ArrayList<>(); if (matchLen > 0) { collectPrefixes(node, Paths.get(""), paths); } return new Match(paths, matchLen); } private static void collectPrefixes(Node node, Path currentPath, List paths) { if (node.isTerminal()) { paths.add(currentPath); } for (Map.Entry child : node.childrenEntrySet()) { var childPath = child.getKey().resolve(currentPath); collectPrefixes(child.getValue(), childPath, paths); } } /** * Since it is very common that a node will have only one child, we save memory by lazily creating a children HashMap only when a second item is added. */ private interface Node { Node[] computeChildrenIfAbsent(Node parent, Path currentNodePath, Path childNodePath); Set> childrenEntrySet(); Node getChild(Path name); void setTerminal(boolean b); boolean isTerminal(); void put(Path path, Node node); } private abstract static class AbstractNode implements Node { private boolean terminal; @Override public final boolean isTerminal() { return terminal; } @Override public final void setTerminal(boolean b) { this.terminal = b; } } private static class SingleChildNode extends AbstractNode { @Nullable private Path singleChildKey; @Nullable private Node singleChildValue; @Override public Node[] computeChildrenIfAbsent(Node parent, Path currentNodePath, Path childNodePath) { if (singleChildKey == null) { put(childNodePath, new SingleChildNode()); return new Node[] {this, singleChildValue}; } if (childNodePath.equals(singleChildKey)) { return new Node[] {this, singleChildValue}; } var child = new SingleChildNode(); var replacement = new MultipleChildrenNode(); replacement.put(singleChildKey, singleChildValue); replacement.put(childNodePath, child); parent.put(currentNodePath, replacement); return new Node[] {replacement, child}; } @Override public Set> childrenEntrySet() { if (singleChildKey == null) { return Collections.emptySet(); } else { return Collections.singleton(new AbstractMap.SimpleEntry<>(singleChildKey, singleChildValue)); } } @Override public void put(Path path, Node node) { this.singleChildKey = path; this.singleChildValue = node; } @Override @CheckForNull public Node getChild(Path name) { return name.equals(singleChildKey) ? singleChildValue : null; } } private static class MultipleChildrenNode extends AbstractNode { private final Map children = new HashMap<>(); @Override public Node[] computeChildrenIfAbsent(Node parent, Path currentNodePath, Path childNodePath) { return new Node[] {this, children.computeIfAbsent(childNodePath, e -> new SingleChildNode())}; } @Override public Set> childrenEntrySet() { return children.entrySet(); } @CheckForNull @Override public Node getChild(Path name) { return children.get(name); } @Override public void put(Path path, Node node) { children.put(path, node); } } public static class Match { private final List paths; private final int matchLen; private Match(List paths, int matchLen) { this.paths = paths; this.matchLen = matchLen; } public List matchPrefixes() { return paths; } public int matchLen() { return matchLen; } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/prefix/package-info.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverconnection.prefix; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/EntityMapper.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Path; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.jooq.JSON; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.commons.storage.model.tables.records.ServerDependencyRisksRecord; import org.sonarsource.sonarlint.core.commons.storage.model.tables.records.ServerFindingsRecord; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.issues.FileLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; public class EntityMapper { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final ObjectMapper objectMapper = new ObjectMapper(); public JSON serializeImpacts(Map impacts) { try { return JSON.valueOf(objectMapper.writeValueAsString(impacts)); } catch (Exception e) { return JSON.valueOf("{}"); } } public JSON serializeFlows(List flows) { try { var flowsToSerialize = flows.stream().map(f -> new TaintFlow(f.locations().stream().map(l -> { var filePath = l.filePath(); var textRangeWithHash = l.textRange(); return new TaintLocation(filePath == null ? null : filePath.toString(), textRangeWithHash == null ? null : new TextRangeWithHash(textRangeWithHash.getStartLine(), textRangeWithHash.getStartLineOffset(), textRangeWithHash.getEndLine(), textRangeWithHash.getEndLineOffset(), textRangeWithHash.getHash()), l.message()); }).toList())).toList(); return JSON.valueOf(objectMapper.writeValueAsString(flowsToSerialize)); } catch (Exception e) { return JSON.valueOf("[]"); } } List deserializeTaintFlows(JSON flows) { try { return objectMapper.readValue(flows.data(), new TypeReference>() { }).stream() .map(flow -> new ServerTaintIssue.Flow(flow.locations.stream() .map(l -> { var textRange = l.textRange; var filePath = l.filePath; return new ServerTaintIssue.ServerIssueLocation(filePath == null ? null : Path.of(filePath), textRange == null ? null : new org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash(textRange.startLine, textRange.startLineOffset, textRange.endLine, textRange.endLineOffset, textRange.hash), l.message); }).toList())) .toList(); } catch (Exception e) { return List.of(); } } // only needed to allow deserializing with Jackson record TaintFlow(List locations) { } record TaintLocation(@Nullable String filePath, @Nullable TextRangeWithHash textRange, @Nullable String message) { } record TextRangeWithHash(int startLine, int startLineOffset, int endLine, int endLineOffset, String hash) { } public JSON serializeTransitions(@Nullable List transitions) { if (transitions == null) { return null; } try { var stringList = transitions.stream().map(Enum::name).toList(); return JSON.valueOf(objectMapper.writeValueAsString(stringList)); } catch (Exception e) { LOG.error("Failed to serialize transitions {}", transitions, e); return JSON.valueOf("{}"); } } public Map deserializeImpacts(@Nullable JSON impactsJson) { if (impactsJson == null) { return Map.of(); } try { var map = objectMapper.readValue(impactsJson.data(), new TypeReference>() { }); return map.entrySet().stream() .collect(Collectors.toMap(entry -> SoftwareQuality.valueOf(entry.getKey()), entry -> ImpactSeverity.valueOf(entry.getValue()))); } catch (Exception e) { LOG.error("Failed to deserialize impacts {}", impactsJson.data(), e); return Map.of(); } } public List deserializeTransitions(@Nullable JSON json) { if (json == null) { return List.of(); } try { var transitions = objectMapper.readValue(json.data(), new TypeReference>() { }); return transitions.stream() .map(transition -> { try { return ServerDependencyRisk.Transition.valueOf(transition); } catch (Exception e) { return null; } }) .filter(Objects::nonNull) .toList(); } catch (Exception e) { LOG.error("Failed to deserialize transitions {}", json.data(), e); return List.of(); } } public Set deserializeLanguages(@Nullable String[] languages) { if (languages == null) { return Set.of(); } return Arrays.stream(languages).map(SonarLanguage::valueOf).collect(Collectors.toSet()); } public String[] serializeLanguages(Set enabledLanguages) { return enabledLanguages.stream().map(Enum::name).toList().toArray(new String[0]); } public ServerFindingsRecord serverIssueToRecord(ServerIssue issue, String branchName, String connectionId, String sonarProjectKey) { var rec = new ServerFindingsRecord(); // Server Issue fields rec.setId(issue.getId()); rec.setServerKey(issue.getKey()); rec.setResolved(issue.isResolved()); var resolutionStatus = issue.getResolutionStatus(); if (resolutionStatus != null) { rec.setIssueResolutionStatus(resolutionStatus.name()); } rec.setRuleKey(issue.getRuleKey()); rec.setMessage(issue.getMessage()); rec.setFilePath(issue.getFilePath().toString()); rec.setCreationDate(toLocalDateTime(issue.getCreationDate())); var userSeverity = issue.getUserSeverity(); if (userSeverity != null) { rec.setUserSeverity(userSeverity.name()); } rec.setRuleType(issue.getType().name()); rec.setImpacts(serializeImpacts(issue.getImpacts())); // Range-Level Issue fields if (issue instanceof RangeLevelServerIssue rangeIssue) { rec.setStartLine(rangeIssue.getTextRange().getStartLine()); rec.setStartLineOffset(rangeIssue.getTextRange().getStartLineOffset()); rec.setEndLine(rangeIssue.getTextRange().getEndLine()); rec.setEndLineOffset(rangeIssue.getTextRange().getEndLineOffset()); rec.setTextRangeHash(rangeIssue.getTextRange().getHash()); } // Line Level Issue fields if (issue instanceof LineLevelServerIssue lineIssue) { rec.setLine(lineIssue.getLine()); rec.setLineHash(lineIssue.getLineHash()); } // Record fields rec.setFindingType(ServerFindingType.ISSUE.name()); rec.setBranchName(branchName); rec.setConnectionId(connectionId); rec.setSonarProjectKey(sonarProjectKey); return rec; } public ServerFindingsRecord serverHotspotToRecord(ServerHotspot hotspot, String branchName, String connectionId, String sonarProjectKey) { var rec = new ServerFindingsRecord(); // Server Hotspot fields rec.setId(hotspot.getId()); rec.setServerKey(hotspot.getKey()); rec.setRuleKey(hotspot.getRuleKey()); rec.setMessage(hotspot.getMessage()); rec.setFilePath(hotspot.getFilePath().toString()); // Text Range fields rec.setStartLine(hotspot.getTextRange().getStartLine()); rec.setStartLineOffset(hotspot.getTextRange().getStartLineOffset()); rec.setEndLine(hotspot.getTextRange().getEndLine()); rec.setEndLineOffset(hotspot.getTextRange().getEndLineOffset()); rec.setCreationDate(toLocalDateTime(hotspot.getCreationDate())); rec.setHotspotReviewStatus(hotspot.getStatus().name()); rec.setVulnerabilityProbability(hotspot.getVulnerabilityProbability().name()); rec.setAssignee(hotspot.getAssignee()); // Record fields rec.setFindingType(ServerFindingType.HOTSPOT.name()); rec.setBranchName(branchName); rec.setConnectionId(connectionId); rec.setSonarProjectKey(sonarProjectKey); return rec; } public ServerFindingsRecord serverTaintToRecord(ServerTaintIssue taint, String branchName, String connectionId, String sonarProjectKey) { var rec = new ServerFindingsRecord(); // Server Taint fields rec.setId(taint.getId()); rec.setServerKey(taint.getSonarServerKey()); rec.setResolved(taint.isResolved()); var resolutionStatus = taint.getResolutionStatus(); if (resolutionStatus != null) { rec.setIssueResolutionStatus(resolutionStatus.name()); } rec.setRuleKey(taint.getRuleKey()); rec.setMessage(taint.getMessage()); rec.setFilePath(taint.getFilePath().toString()); rec.setCreationDate(toLocalDateTime(taint.getCreationDate())); rec.setUserSeverity(taint.getSeverity().name()); rec.setRuleType(taint.getType().name()); rec.setFlows(serializeFlows(taint.getFlows())); var textRange = taint.getTextRange(); if (textRange != null) { rec.setStartLine(textRange.getStartLine()); rec.setStartLineOffset(textRange.getStartLineOffset()); rec.setEndLine(textRange.getEndLine()); rec.setEndLineOffset(textRange.getEndLineOffset()); rec.setTextRangeHash(textRange.getHash()); } rec.setImpacts(serializeImpacts(taint.getImpacts())); rec.setRuleDescriptionContextKey(taint.getRuleDescriptionContextKey()); taint.getCleanCodeAttribute() .ifPresent(codeAttribute -> rec.setCleanCodeAttribute(codeAttribute.name())); // Record fields rec.setFindingType(ServerFindingType.TAINT.name()); rec.setBranchName(branchName); rec.setConnectionId(connectionId); rec.setSonarProjectKey(sonarProjectKey); return rec; } public ServerDependencyRisksRecord serverDependencyRiskToRecord(ServerDependencyRisk risk, String branchName, String connectionId, String sonarProjectKey) { var rec = new ServerDependencyRisksRecord(); // Server Dependency Risk fields rec.setId(risk.key()); rec.setType(risk.type().name()); rec.setSeverity(risk.severity().name()); rec.setSoftwareQuality(risk.quality().name()); rec.setStatus(risk.status().name()); rec.setPackageName(risk.packageName()); rec.setPackageVersion(risk.packageVersion()); rec.setVulnerabilityId(risk.vulnerabilityId()); rec.setCvssScore(risk.cvssScore()); rec.setTransitions(serializeTransitions(risk.transitions())); // Record fields rec.setBranchName(branchName); rec.setConnectionId(connectionId); rec.setSonarProjectKey(sonarProjectKey); return rec; } public ServerIssue adaptIssue(ServerFindingsRecord rec) { var id = rec.getId(); var serverKey = rec.getServerKey(); var ruleKey = rec.getRuleKey(); var message = rec.getMessage(); var filePath = Path.of((rec.getFilePath())); var creationDate = toInstant(rec.getCreationDate()); var userSeverity = rec.getUserSeverity() != null ? IssueSeverity.valueOf(rec.getUserSeverity()) : null; var type = rec.getRuleType() != null ? RuleType.valueOf(rec.getRuleType()) : RuleType.CODE_SMELL; var resolved = Boolean.TRUE.equals(rec.getResolved()); var resolutionStatus = rec.getIssueResolutionStatus() != null ? IssueStatus.valueOf(rec.getIssueResolutionStatus()) : null; var impactsJson = rec.getImpacts(); var impacts = deserializeImpacts(impactsJson); if (rec.getLine() != null) { return new LineLevelServerIssue(id, serverKey, resolved, resolutionStatus, ruleKey, message, rec.getLineHash(), filePath, creationDate, userSeverity, type, rec.getLine(), impacts); } if (rec.getStartLine() != null) { var textRangeWithHash = new org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash(rec.getStartLine(), rec.getStartLineOffset(), rec.getEndLine(), rec.getEndLineOffset(), rec.getTextRangeHash()); return new RangeLevelServerIssue(id, serverKey, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, textRangeWithHash, impacts); } return new FileLevelServerIssue(id, serverKey, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, userSeverity, type, impacts); } public ServerHotspot adaptHotspot(ServerFindingsRecord rec) { var id = rec.getId(); var key = rec.getServerKey(); var ruleKey = rec.getRuleKey(); var message = rec.getMessage(); var filePath = Path.of((rec.getFilePath())); var textRange = new TextRange(rec.getStartLine(), rec.getStartLineOffset(), rec.getEndLine(), rec.getEndLineOffset()); var creationDate = toInstant(rec.getCreationDate()); var status = HotspotReviewStatus.valueOf(rec.getHotspotReviewStatus()); var prob = rec.getVulnerabilityProbability() != null ? VulnerabilityProbability.valueOf(rec.getVulnerabilityProbability()) : VulnerabilityProbability.MEDIUM; var assignee = rec.getAssignee(); return new ServerHotspot(id, key, ruleKey, message, filePath, textRange, creationDate, status, prob, assignee); } public ServerTaintIssue adaptTaint(ServerFindingsRecord rec) { var id = rec.getId(); var key = rec.getServerKey(); var resolved = Boolean.TRUE.equals(rec.getResolved()); var resolutionStatus = rec.getIssueResolutionStatus() != null ? IssueStatus.valueOf(rec.getIssueResolutionStatus()) : null; var ruleKey = rec.getRuleKey(); var message = rec.getMessage(); var filePath = Path.of(rec.getFilePath()); var creationDate = toInstant(rec.getCreationDate()); var severity = rec.getUserSeverity() != null ? IssueSeverity.valueOf(rec.getUserSeverity()) : IssueSeverity.MAJOR; var type = rec.getRuleType() != null ? RuleType.valueOf(rec.getRuleType()) : RuleType.CODE_SMELL; org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash textRangeWithHash = null; if (rec.getStartLine() != null) { textRangeWithHash = new org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash(rec.getStartLine(), rec.getStartLineOffset(), rec.getEndLine(), rec.getEndLineOffset(), rec.getTextRangeHash()); } var ruleDescCtx = rec.getRuleDescriptionContextKey(); var cleanCodeAttr = rec.getCleanCodeAttribute() != null ? CleanCodeAttribute.valueOf(rec.getCleanCodeAttribute()) : null; var impactsJson = rec.getImpacts(); var impacts = deserializeImpacts(impactsJson); var flows = deserializeTaintFlows(rec.getFlows()); return new ServerTaintIssue(id, key, resolved, resolutionStatus, ruleKey, message, filePath, creationDate, severity, type, textRangeWithHash, ruleDescCtx, cleanCodeAttr, impacts, flows); } private static Instant toInstant(LocalDateTime ldt) { return ldt.toInstant(ZoneOffset.UTC); } static LocalDateTime toLocalDateTime(Instant instant) { return LocalDateTime.ofInstant(instant, ZoneOffset.UTC); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/HotspotReviewStatusBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; public class HotspotReviewStatusBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return HotspotReviewStatus.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final HotspotReviewStatus cPair = (HotspotReviewStatus) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80_000_000); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/InstantBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import java.time.Instant; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; public class InstantBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return Instant.ofEpochMilli(BindingUtils.readLong(stream)); } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final var instant = (Instant) object; output.writeUnsignedLong(instant.toEpochMilli() ^ 0x8_000_000_000_000_000L); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/IssueSeverityBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.IssueSeverity; public class IssueSeverityBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return IssueSeverity.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final IssueSeverity cPair = (IssueSeverity) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80000000); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/IssueStatusBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.sonarsource.sonarlint.core.commons.IssueStatus; public class IssueStatusBinding extends ComparableBinding { @Override public IssueStatus readObject(ByteArrayInputStream stream) { return IssueStatus.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(LightOutputStream output, Comparable object) { final IssueStatus cPair = (IssueStatus) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80_000_000); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/IssueTypeBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; import org.sonarsource.sonarlint.core.commons.RuleType; public class IssueTypeBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return RuleType.values()[BindingUtils.readInt(stream)]; } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final RuleType cPair = (RuleType) object; output.writeUnsignedInt(cPair.ordinal() ^ 0x80_000_000); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/NewCodeDefinitionStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.commons.NewCodeMode; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class NewCodeDefinitionStorage { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String NEW_CODE_DEFINITION_PB = "new_code_definition.pb"; private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public NewCodeDefinitionStorage(Path rootPath) { this.storageFilePath = rootPath.resolve(NEW_CODE_DEFINITION_PB); } public void store(NewCodeDefinition newCodeDefinition) { FileUtils.mkdirs(storageFilePath.getParent()); var newCodeDefinitionToStore = adapt(newCodeDefinition); LOG.debug("Storing new code definition in {}", storageFilePath); rwLock.write(() -> writeToFile(newCodeDefinitionToStore, storageFilePath)); } public Optional read() { return rwLock.read(() -> Files.exists(storageFilePath) ? Optional.of(adapt(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.NewCodeDefinition.parser()))) : Optional.empty()); } static Sonarlint.NewCodeDefinition adapt(NewCodeDefinition newCodeDefinition) { var builder = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.valueOf(newCodeDefinition.getMode().name())); if (newCodeDefinition.getMode() == NewCodeMode.NUMBER_OF_DAYS) { var newCodeNumberOfDays = (NewCodeDefinition.NewCodeNumberOfDaysWithDate) newCodeDefinition; builder.setDays(newCodeNumberOfDays.getDays()); } if (newCodeDefinition.getMode() != NewCodeMode.REFERENCE_BRANCH) { var newCodeDefinitionWithDate = (NewCodeDefinition.NewCodeDefinitionWithDate) newCodeDefinition; builder.setThresholdDate(newCodeDefinitionWithDate.getThresholdDate().toEpochMilli()); } else { var newCodeReferenceBranch = (NewCodeDefinition.NewCodeReferenceBranch) newCodeDefinition; builder.setReferenceBranch(newCodeReferenceBranch.getBranchName()); } if (newCodeDefinition.getMode() == NewCodeMode.PREVIOUS_VERSION) { var newCodePreviousVersion = (NewCodeDefinition.NewCodePreviousVersion) newCodeDefinition; var version = newCodePreviousVersion.getVersion(); if (version != null) { builder.setVersion(version); } } return builder.build(); } static NewCodeDefinition adapt(Sonarlint.NewCodeDefinition newCodeDefinition) { var thresholdDate = newCodeDefinition.getThresholdDate(); var mode = newCodeDefinition.getMode(); switch (mode) { case NUMBER_OF_DAYS: return NewCodeDefinition.withNumberOfDaysWithDate(newCodeDefinition.getDays(), thresholdDate); case PREVIOUS_VERSION: var version = newCodeDefinition.hasVersion() ? newCodeDefinition.getVersion() : null; return NewCodeDefinition.withPreviousVersion(thresholdDate, version); case REFERENCE_BRANCH: return NewCodeDefinition.withReferenceBranch(newCodeDefinition.getReferenceBranch()); case SPECIFIC_ANALYSIS: return NewCodeDefinition.withSpecificAnalysis(thresholdDate); default: throw new IllegalArgumentException("Unsupported mode: " + mode); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/OrganizationStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; import java.util.UUID; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.Organization; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class OrganizationStorage { public static final String ORGANIZATION_PB = "organization.pb"; private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public OrganizationStorage(Path rootPath) { this.storageFilePath = rootPath.resolve(ORGANIZATION_PB); } public void store(Organization organization) { FileUtils.mkdirs(storageFilePath.getParent()); var settingsToStore = adapt(organization); LOG.debug("Storing organization settings in {}", storageFilePath); rwLock.write(() -> writeToFile(settingsToStore, storageFilePath)); LOG.debug("Stored organization settings"); } public Optional read() { return rwLock.read(() -> Files.exists(storageFilePath) ? Optional.of(adapt(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.Organization.parser()))) : Optional.empty()); } private static Sonarlint.Organization adapt(Organization organization) { return Sonarlint.Organization.newBuilder().setId(organization.id()).setUuidV4(organization.uuidV4().toString()).build(); } private static Organization adapt(Sonarlint.Organization organization) { return new Organization(organization.getId(), UUID.fromString(organization.getUuidV4())); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/PluginsStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.plugins.ServerPlugin; import org.sonarsource.sonarlint.core.serverconnection.StoredPlugin; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; public class PluginsStorage { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String PLUGIN_REFERENCES_PB = "plugin_references.pb"; private final Path rootPath; private final Path pluginReferencesFilePath; private final RWLock rwLock = new RWLock(); public PluginsStorage(Path connectionStorageRoot) { this.rootPath = connectionStorageRoot.resolve("plugins"); this.pluginReferencesFilePath = rootPath.resolve(PLUGIN_REFERENCES_PB); } public boolean isValid() { if (!Files.exists(pluginReferencesFilePath)) { return false; } try { rwLock.read(() -> ProtobufFileUtil.readFile(pluginReferencesFilePath, Sonarlint.PluginReferences.parser())); return true; } catch (Exception e) { LOG.debug("Could not load plugins storage", e); return false; } } public void store(ServerPlugin plugin, InputStream pluginBinary) { rwLock.write(() -> { try { var pluginPath = rootPath.resolve(plugin.getFilename()); FileUtils.copyInputStreamToFile(pluginBinary, pluginPath.toFile()); LOG.debug("Storing plugin to {} with file size {} bytes", pluginPath.toAbsolutePath(), Files.size(pluginPath)); var pluginFile = pluginPath.toFile(); LOG.debug("Plugin file created: {}", pluginFile.exists()); LOG.debug("Written plugin file size {} bytes", Files.size(pluginPath)); var reference = adapt(plugin); var references = Files.exists(pluginReferencesFilePath) ? ProtobufFileUtil.readFile(pluginReferencesFilePath, Sonarlint.PluginReferences.parser()) : Sonarlint.PluginReferences.newBuilder().build(); var currentReferences = Sonarlint.PluginReferences.newBuilder(references); currentReferences.putPluginsByKey(plugin.getKey(), reference); ProtobufFileUtil.writeToFile(currentReferences.build(), pluginReferencesFilePath); LOG.debug("Plugin file {} created: {}", pluginReferencesFilePath, pluginReferencesFilePath.toFile().exists()); } catch (IOException e) { // XXX should we stop the whole sync ? just continue and log ? throw new StorageException("Cannot save plugin " + plugin.getFilename() + " in " + rootPath, e); } }); } public List getStoredPlugins() { return rwLock.read(() -> Files.exists(pluginReferencesFilePath) ? ProtobufFileUtil.readFile(pluginReferencesFilePath, Sonarlint.PluginReferences.parser()) : Sonarlint.PluginReferences.newBuilder().build()).getPluginsByKeyMap().values().stream().map(this::adapt).toList(); } public Map getStoredPluginsByKey() { return byKey(getStoredPlugins()); } public Map getStoredPluginPathsByKey() { return getStoredPluginsByKey().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().getJarPath())); } private static Map byKey(List plugins) { return plugins.stream().collect(Collectors.toMap(StoredPlugin::getKey, Function.identity())); } private static Sonarlint.PluginReferences.PluginReference adapt(ServerPlugin plugin) { return Sonarlint.PluginReferences.PluginReference.newBuilder() .setKey(plugin.getKey()) .setHash(plugin.getHash()) .setFilename(plugin.getFilename()) .build(); } private StoredPlugin adapt(Sonarlint.PluginReferences.PluginReference plugin) { return new StoredPlugin( plugin.getKey(), plugin.getHash(), rootPath.resolve(plugin.getFilename())); } public void cleanUpUnknownPlugins(List serverPluginsExpectedInStorage) { var expectedPluginPaths = serverPluginsExpectedInStorage.stream().map(plugin -> rootPath.resolve(plugin.getFilename())).collect(Collectors.toSet()); var pluginsByKey = serverPluginsExpectedInStorage.stream().collect(Collectors.toMap(ServerPlugin::getKey, PluginsStorage::adapt)); var currentReferences = Sonarlint.PluginReferences.newBuilder(); currentReferences.putAllPluginsByKey(pluginsByKey); rwLock.write(() -> { var unknownFiles = getUnknownFiles(expectedPluginPaths); deleteFiles(unknownFiles); createPluginDirectory(); ProtobufFileUtil.writeToFile(currentReferences.build(), pluginReferencesFilePath); }); } private void deleteFiles(List unknownFiles) { if (!unknownFiles.isEmpty()) { LOG.debug("Cleaning up the plugins storage {}, removing {} unknown files:", rootPath, unknownFiles.size()); unknownFiles.forEach(f -> LOG.debug(f.getAbsolutePath())); unknownFiles.forEach(FileUtils::deleteQuietly); } } private List getUnknownFiles(Set knownPluginsPaths) { if (!Files.exists(rootPath)) { return Collections.emptyList(); } LOG.debug("Known plugin paths: {}", knownPluginsPaths); try (Stream pathsInDir = Files.list(rootPath)) { var paths = pathsInDir.toList(); LOG.debug("Paths in dir: {}", paths); var unknownFiles = paths.stream() .filter(p -> !p.equals(pluginReferencesFilePath)) .filter(p -> !knownPluginsPaths.contains(p)) .map(Path::toFile) .toList(); LOG.debug("Unknown files: {}", unknownFiles); return unknownFiles; } catch (Exception e) { LOG.error("Cannot list files in '{}'", rootPath, e); return Collections.emptyList(); } } public void storeNoPlugins() { if (!Files.exists(pluginReferencesFilePath)) { createPluginDirectory(); rwLock.write(() -> { var references = Sonarlint.PluginReferences.newBuilder().build(); ProtobufFileUtil.writeToFile(references, pluginReferencesFilePath); }); } } private void createPluginDirectory() { try { Files.createDirectories(pluginReferencesFilePath.getParent()); } catch (IOException e) { throw new StorageException(String.format("Cannot create plugin file directory: %s", rootPath), e); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ProjectServerIssueStore.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerFinding; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; public interface ProjectServerIssueStore { boolean wasEverUpdated(); /** * Store issues for a branch by replacing existing ones. */ void replaceAllIssuesOfBranch(String branchName, List> issues, Set enabledLanguages); void replaceAllHotspotsOfBranch(String branchName, Collection serverHotspots, Set enabledLanguages); void replaceAllHotspotsOfFile(String branchName, Path serverFilePath, Collection serverHotspots); /** * Update the resolution status of a hotspot by its key. * @return true if the hotspot with the given key was found, else false */ boolean changeHotspotStatus(String hotspotKey, HotspotReviewStatus newStatus); /** * Store issues for a single file by replacing existing ones and moving issues if necessary. */ void replaceAllIssuesOfFile(String branchName, Path serverFilePath, List> issues); /** * Merge provided issues to stored ones for the given project: * - new issues are added * - existing issues are updated * - closed issues are removed from the store */ void mergeIssues(String branchName, List> issuesToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp, Set enabledLanguages); /** * Merge provided taint issues to stored ones for the given project: * - new issues are added * - existing issues are updated * - closed issues are removed from the store */ void mergeTaintIssues(String branchName, List issuesToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp, Set enabledLanguages); /** * Merge provided hotspots to stored ones for the given project: * - new hotspots are added * - existing hotspots are updated * - closed hotspots are removed from the store */ void mergeHotspots(String branchName, List hotspotsToMerge, Set closedHotspotKeysToDelete, Instant syncTimestamp, Set enabledLanguages); /** * Return the timestamp of the last issue sync for a given branch. * @return empty if the issues of the branch have never been pulled */ Optional getLastIssueSyncTimestamp(String branchName); /** * Return the last enabled languages of the last issue sync for a given branch. * * @return empty if the issues of the branch have never been pulled */ Set getLastIssueEnabledLanguages(String branchName); /** * Return the last enabled languages of the last taint sync for a given branch. * @return empty if the taints of the branch have never been pulled */ Set getLastTaintEnabledLanguages(String branchName); /** * Return the last enabled languages of the last hotspot sync for a given branch. * @return empty if the hotspots of the branch have never been pulled */ Set getLastHotspotEnabledLanguages(String branchName); /** * Return the timestamp of the last taint issue sync for a given branch. * @return empty if the taint issues of the branch have never been pulled */ Optional getLastTaintSyncTimestamp(String branchName); /** * Return the timestamp of the last hotspot sync for a given branch. * @return empty if the hotspots of the branch have never been pulled */ Optional getLastHotspotSyncTimestamp(String branchName); /** * Load issues stored for specified file. * * @param branchName * @param sqFilePath the relative path to the base of project, in SonarQube * @return issues, possibly empty */ List> load(String branchName, Path sqFilePath); /** * Store taint issues for a branch. */ void replaceAllTaintsOfBranch(String branchName, List taintIssues, Set enabledLanguages); /** * Load hotspots stored for specified file. * * @param serverFilePath the relative path to the base of project, from the server point of view * @return issues, possibly empty */ Collection loadHotspots(String branchName, Path serverFilePath); /** * Load all taint issues stored for a branch. * * * @param branchName * @return issues, possibly empty */ List loadTaint(String branchName); /** * @param issueKey * @param issueUpdater * @return true if the issue with the corresponding key exists in the store and has been updated */ boolean updateIssue(String issueKey, Consumer> issueUpdater); /** * Retrieve an issue from the store * @param issueKey * @return the server issue if found, null otherwise */ ServerIssue getIssue(String issueKey); /** * Retrieve a hotspot from the store * @param hotspotKey * @return the hotspot if found, null otherwise */ ServerHotspot getHotspot(String hotspotKey); /** * Set the resolution status of an Issue (by its key). */ Optional updateIssueResolutionStatus(String issueKey, boolean isTaintIssue, boolean isResolved); /** * @return the updated issue if found, else empty */ Optional updateTaintIssueBySonarServerKey(String sonarServerKey, Consumer taintIssueUpdater); void insert(String branchName, ServerTaintIssue taintIssue); void insert(String branchName, ServerHotspot hotspot); /** * @return the id of the deleted taint issue if it was found */ Optional deleteTaintIssueBySonarServerKey(String sonarServerKeyToDelete); void deleteHotspot(String hotspotKey); void updateHotspot(String hotspotKey, Consumer hotspotUpdater); boolean containsIssue(String issueKey); /** * Store dependency risks for a branch by replacing existing ones. */ void replaceAllDependencyRisksOfBranch(String branchName, List serverDependencyRisks); /** * Load all dependency risks stored for a branch. */ List loadDependencyRisks(String branchName); void updateDependencyRiskStatus(UUID key, ServerDependencyRisk.Status newStatus, List transitions); void removeFindingsForConnection(String connectionId); } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ProjectStoragePaths.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.charset.StandardCharsets; import org.apache.commons.codec.binary.Hex; public class ProjectStoragePaths { private static final int MAX_FOLDER_NAME_SIZE = 255; /** * Encodes a string to be used as a valid file or folder name. * It should work in all OS and different names should never collide. * See SLCORE-148 and SLCORE-228. */ public static String encodeForFs(String name) { var encoded = Hex.encodeHexString(name.getBytes(StandardCharsets.UTF_8)); if (encoded.length() > MAX_FOLDER_NAME_SIZE) { // Most FS will not support a folder name greater than 255 var md5 = org.apache.commons.codec.digest.DigestUtils.md5Hex(name); return encoded.substring(0, MAX_FOLDER_NAME_SIZE - md5.length()) + md5; } return encoded; } private ProjectStoragePaths() { // utility class } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ProtobufFileUtil.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import com.google.protobuf.Message; import com.google.protobuf.Parser; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class ProtobufFileUtil { private ProtobufFileUtil() { // only static stuff } public static T readFile(Path file, Parser parser) { try (var input = Files.newInputStream(file)) { return parser.parseFrom(input); } catch (IOException e) { throw new StorageException("Failed to read file: " + file, e); } } public static void writeToFile(Message message, Path toFile) { try (var out = Files.newOutputStream(toFile)) { message.writeTo(out); out.flush(); } catch (IOException e) { throw new StorageException("Unable to write protocol buffer data to file " + toFile, e); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/RWLock.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; public class RWLock { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public T read(Supplier supplier) { readWriteLock.readLock().lock(); try { return supplier.get(); } finally { readWriteLock.readLock().unlock(); } } public void write(Runnable runnable) { readWriteLock.writeLock().lock(); try { runnable.run(); } finally { readWriteLock.writeLock().unlock(); } } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerFindingRepository.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; import org.jooq.Configuration; import org.jooq.DSLContext; import org.jooq.Record1; import org.jooq.TableField; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.storage.model.Tables; import org.sonarsource.sonarlint.core.commons.storage.model.tables.records.ServerBranchesRecord; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerFinding; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_BRANCHES; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_DEPENDENCY_RISKS; import static org.sonarsource.sonarlint.core.commons.storage.model.Tables.SERVER_FINDINGS; public class ServerFindingRepository implements ProjectServerIssueStore { private final EntityMapper mapper = new EntityMapper(); private final DSLContext database; private final String connectionId; private final String sonarProjectKey; public ServerFindingRepository(DSLContext database, String connectionId, String sonarProjectKey) { this.database = database; this.connectionId = connectionId; this.sonarProjectKey = sonarProjectKey; } @Override public boolean wasEverUpdated() { return database.fetchExists( database.selectFrom(SERVER_BRANCHES) .where(SERVER_BRANCHES.CONNECTION_ID.eq(connectionId) .and(SERVER_BRANCHES.SONAR_PROJECT_KEY.eq(sonarProjectKey)) .and(SERVER_BRANCHES.LAST_ISSUE_SYNC_TS.isNotNull().or(SERVER_BRANCHES.LAST_HOTSPOT_SYNC_TS.isNotNull()).or(SERVER_BRANCHES.LAST_TAINT_SYNC_TS.isNotNull())))); } @Override public void replaceAllIssuesOfBranch(String branchName, List> issues, Set enabledLanguages) { database.transaction(trx -> { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeIssues(branchName, connectionId, sonarProjectKey, trx, issues); }); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_ISSUE_SYNC_TS, SERVER_BRANCHES.LAST_ISSUE_ENABLED_LANGS, Instant.now(), enabledLanguages); } @Override public void replaceAllHotspotsOfBranch(String branchName, Collection serverHotspots, Set enabledLanguages) { database.transaction(trx -> { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeHotspots(branchName, connectionId, sonarProjectKey, trx, serverHotspots); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_HOTSPOT_SYNC_TS, SERVER_BRANCHES.LAST_HOTSPOT_ENABLED_LANGS, Instant.now(), enabledLanguages); }); } @Override public void replaceAllHotspotsOfFile(String branchName, Path serverFilePath, Collection serverHotspots) { database.transaction(trx -> { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FILE_PATH.eq(serverFilePath.toString())) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeHotspots(branchName, connectionId, sonarProjectKey, trx, serverHotspots); }); } // we don't consume return value for now, probably should be void @Override public boolean changeHotspotStatus(String hotspotKey, HotspotReviewStatus newStatus) { database.update(SERVER_FINDINGS) .set(SERVER_FINDINGS.HOTSPOT_REVIEW_STATUS, newStatus.name()) .where(SERVER_FINDINGS.SERVER_KEY.eq(hotspotKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); return true; } @Override public void replaceAllIssuesOfFile(String branchName, Path serverFilePath, List> issues) { database.transaction(trx -> { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FILE_PATH.eq(serverFilePath.toString())) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeIssues(branchName, connectionId, sonarProjectKey, trx, issues); }); } @Override public void mergeIssues(String branchName, List> issuesToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp, Set enabledLanguages) { database.transaction(trx -> { if (!closedIssueKeysToDelete.isEmpty()) { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.SERVER_KEY.in(closedIssueKeysToDelete)) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); } var issueIds = issuesToMerge.stream().map(ServerIssue::getId).collect(Collectors.toSet()); trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.ID.in(issueIds)) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey)) .execute(); batchMergeIssues(branchName, connectionId, sonarProjectKey, trx, issuesToMerge); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_ISSUE_SYNC_TS, SERVER_BRANCHES.LAST_ISSUE_ENABLED_LANGS, syncTimestamp, enabledLanguages); }); } @Override public void mergeTaintIssues(String branchName, List taintsToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp, Set enabledLanguages) { database.transaction(trx -> { if (!closedIssueKeysToDelete.isEmpty()) { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.TAINT.name())) .and(SERVER_FINDINGS.SERVER_KEY.in(closedIssueKeysToDelete)) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); } batchMergeTaints(branchName, connectionId, sonarProjectKey, trx, taintsToMerge); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_TAINT_SYNC_TS, SERVER_BRANCHES.LAST_TAINT_ENABLED_LANGS, syncTimestamp, enabledLanguages); }); } @Override public void mergeHotspots(String branchName, List hotspotsToMerge, Set closedHotspotKeysToDelete, Instant syncTimestamp, Set enabledLanguages) { database.transaction(trx -> { if (!closedHotspotKeysToDelete.isEmpty()) { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.SERVER_KEY.in(closedHotspotKeysToDelete)) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); } batchMergeHotspots(branchName, connectionId, sonarProjectKey, trx, hotspotsToMerge); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_HOTSPOT_SYNC_TS, SERVER_BRANCHES.LAST_HOTSPOT_ENABLED_LANGS, syncTimestamp, enabledLanguages); }); } @Override public Optional getLastIssueSyncTimestamp(String branchName) { return database.select(SERVER_BRANCHES.LAST_ISSUE_SYNC_TS) .from(SERVER_BRANCHES) .where(SERVER_BRANCHES.BRANCH_NAME.eq(branchName) .and(SERVER_BRANCHES.CONNECTION_ID.eq(connectionId)) .and(SERVER_BRANCHES.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOptional() .map(Record1::value1) .map(ServerFindingRepository::toInstant); } @Override public Set getLastIssueEnabledLanguages(String branchName) { return readLanguages(branchName, SERVER_BRANCHES.LAST_ISSUE_ENABLED_LANGS); } @Override public Set getLastTaintEnabledLanguages(String branchName) { return readLanguages(branchName, SERVER_BRANCHES.LAST_TAINT_ENABLED_LANGS); } @Override public Set getLastHotspotEnabledLanguages(String branchName) { return readLanguages(branchName, SERVER_BRANCHES.LAST_HOTSPOT_ENABLED_LANGS); } @Override public Optional getLastTaintSyncTimestamp(String branchName) { return database.select(SERVER_BRANCHES.LAST_TAINT_SYNC_TS) .from(SERVER_BRANCHES) .where(SERVER_BRANCHES.BRANCH_NAME.eq(branchName) .and(SERVER_BRANCHES.CONNECTION_ID.eq(connectionId)) .and(SERVER_BRANCHES.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOptional() .map(Record1::value1) .map(ServerFindingRepository::toInstant); } @Override public Optional getLastHotspotSyncTimestamp(String branchName) { return database.select(SERVER_BRANCHES.LAST_HOTSPOT_SYNC_TS) .from(SERVER_BRANCHES) .where(SERVER_BRANCHES.BRANCH_NAME.eq(branchName) .and(SERVER_BRANCHES.CONNECTION_ID.eq(connectionId)) .and(SERVER_BRANCHES.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOptional() .map(Record1::value1) .map(ServerFindingRepository::toInstant); } private static Instant toInstant(LocalDateTime ldt) { return ldt.toInstant(ZoneOffset.UTC); } private void upsertBranchMetadata(String branchName, TableField lastSyncField, TableField enabledLanguagesField, Instant syncTimestamp, Set enabledLanguages) { var lastSyncTime = LocalDateTime.ofInstant(syncTimestamp, ZoneOffset.UTC); var enabledLanguagesAsJson = mapper.serializeLanguages(enabledLanguages); database.insertInto(SERVER_BRANCHES, SERVER_BRANCHES.BRANCH_NAME, SERVER_BRANCHES.CONNECTION_ID, SERVER_BRANCHES.SONAR_PROJECT_KEY, lastSyncField, enabledLanguagesField) .values(branchName, connectionId, sonarProjectKey, lastSyncTime, enabledLanguagesAsJson) .onDuplicateKeyUpdate() .set(lastSyncField, lastSyncTime) .set(enabledLanguagesField, enabledLanguagesAsJson) .execute(); } private Set readLanguages(String branchName, TableField langsField) { var table = SERVER_BRANCHES; var rec = database.select(langsField) .from(table) .where(table.BRANCH_NAME.eq(branchName) .and(table.CONNECTION_ID.eq(connectionId)) .and(table.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOne(); if (rec == null) { return Set.of(); } return mapper.deserializeLanguages(rec.value1()); } @Override public List> load(String branchName, Path sqFilePath) { return database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FILE_PATH.eq(sqFilePath.toString())) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetch().stream() .>map(mapper::adaptIssue) .toList(); } @Override public void replaceAllTaintsOfBranch(String branchName, List taintIssues, Set enabledLanguages) { database.transaction(trx -> { trx.dsl().deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.TAINT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeTaints(branchName, connectionId, sonarProjectKey, trx, taintIssues); upsertBranchMetadata(branchName, SERVER_BRANCHES.LAST_TAINT_SYNC_TS, SERVER_BRANCHES.LAST_TAINT_ENABLED_LANGS, Instant.now(), enabledLanguages); }); } @Override public Collection loadHotspots(String branchName, Path serverFilePath) { return database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FILE_PATH.eq(serverFilePath.toString())) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetch().stream() .map(mapper::adaptHotspot) .toList(); } @Override public List loadTaint(String branchName) { return database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.BRANCH_NAME.eq(branchName) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.TAINT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetch().stream() .map(mapper::adaptTaint) .toList(); } @Override public boolean updateIssue(String issueKey, Consumer> issueUpdater) { var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOne(); if (rec == null) { return false; } var current = mapper.adaptIssue(rec); issueUpdater.accept(current); database.update(SERVER_FINDINGS) .set(SERVER_FINDINGS.RESOLVED, current.isResolved()) .set(SERVER_FINDINGS.USER_SEVERITY, current.getUserSeverity() != null ? current.getUserSeverity().name() : null) .set(SERVER_FINDINGS.RULE_TYPE, current.getType() != null ? current.getType().name() : null) .set(SERVER_FINDINGS.IMPACTS, mapper.serializeImpacts(current.getImpacts())) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); return true; } @Override public ServerIssue getIssue(String issueKey) { var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.ISSUE.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOne(); return rec != null ? mapper.adaptIssue(rec) : null; } @Override public ServerHotspot getHotspot(String hotspotKey) { var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(hotspotKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name())) .and(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .and(SERVER_FINDINGS.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetchOne(); return rec != null ? mapper.adaptHotspot(rec) : null; } @Override public Optional updateIssueResolutionStatus(String issueKey, boolean isTaintIssue, boolean isResolved) { var type = isTaintIssue ? ServerFindingType.TAINT.name() : ServerFindingType.ISSUE.name(); database.update(SERVER_FINDINGS) .set(SERVER_FINDINGS.RESOLVED, isResolved) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(type))) .execute(); var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(type))) .fetchOne(); if (rec == null) { return Optional.empty(); } return Optional.of(isTaintIssue ? mapper.adaptTaint(rec) : mapper.adaptIssue(rec)); } @Override public Optional updateTaintIssueBySonarServerKey(String sonarServerKey, Consumer taintIssueUpdater) { var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(sonarServerKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.TAINT.name()))) .fetchOne(); if (rec == null) { return Optional.empty(); } var current = mapper.adaptTaint(rec); taintIssueUpdater.accept(current); database.update(SERVER_FINDINGS) .set(SERVER_FINDINGS.USER_SEVERITY, current.getSeverity().name()) .set(SERVER_FINDINGS.RULE_TYPE, current.getType().name()) .set(SERVER_FINDINGS.RESOLVED, current.isResolved()) .set(SERVER_FINDINGS.IMPACTS, mapper.serializeImpacts(current.getImpacts())) .where(SERVER_FINDINGS.SERVER_KEY.eq(sonarServerKey)) .execute(); return Optional.of(current); } @Override public void insert(String branchName, ServerTaintIssue taintIssue) { database.transaction(trx -> batchMergeTaints(branchName, connectionId, sonarProjectKey, trx, List.of(taintIssue))); } @Override public void insert(String branchName, ServerHotspot hotspot) { database.transaction(trx -> batchMergeHotspots(branchName, connectionId, sonarProjectKey, trx, List.of(hotspot))); } @Override public Optional deleteTaintIssueBySonarServerKey(String sonarServerKeyToDelete) { var rec = database.select(SERVER_FINDINGS.ID) .from(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(sonarServerKeyToDelete) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.TAINT.name()))) .fetchOne(); if (rec == null) { return Optional.empty(); } var idStr = rec.get(0, UUID.class); database.deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.ID.eq(idStr)) .execute(); return Optional.of(idStr); } @Override public void deleteHotspot(String hotspotKey) { database.deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(hotspotKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name()))) .execute(); } @Override public void updateHotspot(String hotspotKey, Consumer hotspotUpdater) { var rec = database.selectFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(hotspotKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name()))) .fetchOne(); if (rec == null) { return; } var current = mapper.adaptHotspot(rec); hotspotUpdater.accept(current); database.update(SERVER_FINDINGS) .set(SERVER_FINDINGS.HOTSPOT_REVIEW_STATUS, current.getStatus().name()) .set(SERVER_FINDINGS.ASSIGNEE, current.getAssignee()) .where(SERVER_FINDINGS.SERVER_KEY.eq(hotspotKey) .and(SERVER_FINDINGS.FINDING_TYPE.eq(ServerFindingType.HOTSPOT.name()))) .execute(); } @Override public boolean containsIssue(String issueKey) { var cnt = database.selectCount().from(SERVER_FINDINGS) .where(SERVER_FINDINGS.SERVER_KEY.eq(issueKey) .and(SERVER_FINDINGS.FINDING_TYPE.in(ServerFindingType.ISSUE.name(), ServerFindingType.TAINT.name()))) .fetchOne(); return cnt != null && cnt.value1() > 0; } @Override public void replaceAllDependencyRisksOfBranch(String branchName, List serverDependencyRisks) { var table = SERVER_DEPENDENCY_RISKS; database.transaction(trx -> { trx.dsl().deleteFrom(table) .where(table.BRANCH_NAME.eq(branchName) .and(table.CONNECTION_ID.eq(connectionId)) .and(table.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .execute(); batchMergeDependencyRisks(branchName, connectionId, sonarProjectKey, trx, serverDependencyRisks); }); } @Override public List loadDependencyRisks(String branchName) { var table = SERVER_DEPENDENCY_RISKS; return database.select(table.ID, table.TYPE, table.SEVERITY, table.SOFTWARE_QUALITY, table.STATUS, table.PACKAGE_NAME, table.PACKAGE_VERSION, table.VULNERABILITY_ID, table.CVSS_SCORE, table.TRANSITIONS) .from(table) .where(table.BRANCH_NAME.eq(branchName) .and(table.CONNECTION_ID.eq(connectionId)) .and(table.SONAR_PROJECT_KEY.eq(sonarProjectKey))) .fetch() .stream() .map(rec -> { var id = rec.get(table.ID); var type = ServerDependencyRisk.Type.valueOf(rec.get(table.TYPE)); var severity = ServerDependencyRisk.Severity.valueOf(rec.get(table.SEVERITY)); var quality = ServerDependencyRisk.SoftwareQuality.valueOf(rec.get(table.SOFTWARE_QUALITY)); var status = ServerDependencyRisk.Status.valueOf(rec.get(table.STATUS)); var pkg = rec.get(table.PACKAGE_NAME); var ver = rec.get(table.PACKAGE_VERSION); var vuln = rec.get(table.VULNERABILITY_ID); var cvss = rec.get(table.CVSS_SCORE); var transitions = mapper.deserializeTransitions(rec.get(table.TRANSITIONS)); return new ServerDependencyRisk(id, type, severity, quality, status, pkg, ver, vuln, cvss, transitions); }) .toList(); } @Override public void updateDependencyRiskStatus(UUID key, ServerDependencyRisk.Status newStatus, List transitions) { var table = Tables.SERVER_DEPENDENCY_RISKS; database.update(table) .set(table.STATUS, newStatus.name()) .set(table.TRANSITIONS, mapper.serializeTransitions(transitions)) .where(table.ID.eq(key)) .execute(); } @Override public void removeFindingsForConnection(String connectionId) { database.deleteFrom(SERVER_FINDINGS) .where(SERVER_FINDINGS.CONNECTION_ID.eq(connectionId)) .execute(); database.deleteFrom(SERVER_DEPENDENCY_RISKS) .where(SERVER_DEPENDENCY_RISKS.CONNECTION_ID.eq(connectionId)) .execute(); database.deleteFrom(SERVER_BRANCHES) .where(SERVER_BRANCHES.CONNECTION_ID.eq(connectionId)) .execute(); } private void batchMergeHotspots(String branchName, String connectionId, String sonarProjectKey, Configuration trx, Collection hotspots) { trx.dsl().batchMerge(hotspots.stream() .map(hotspot -> mapper.serverHotspotToRecord(hotspot, branchName, connectionId, sonarProjectKey)).toList()) .execute(); } private void batchMergeIssues(String branchName, String connectionId, String sonarProjectKey, Configuration trx, Collection> issues) { trx.dsl().batchMerge(issues.stream() .map(issue -> mapper.serverIssueToRecord(issue, branchName, connectionId, sonarProjectKey)).toList()) .execute(); } private void batchMergeTaints(String branchName, String connectionId, String sonarProjectKey, Configuration trx, Collection taints) { trx.dsl().batchMerge(taints.stream() .map(taint -> mapper.serverTaintToRecord(taint, branchName, connectionId, sonarProjectKey)).toList()) .execute(); } private void batchMergeDependencyRisks(String branchName, String connectionId, String sonarProjectKey, Configuration trx, Collection risks) { trx.dsl().batchMerge(risks.stream() .map(risk -> mapper.serverDependencyRiskToRecord(risk, branchName, connectionId, sonarProjectKey)).toList()) .execute(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerFindingType.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; public enum ServerFindingType { ISSUE, HOTSPOT, TAINT } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerInfoStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import org.sonarsource.sonarlint.core.serverapi.system.ServerStatusInfo; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.ServerSettings; import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.sonarsource.sonarlint.core.serverconnection.ServerSettings.MQR_MODE_SETTING; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class ServerInfoStorage { public static final String SERVER_INFO_PB = "server_info.pb"; private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public ServerInfoStorage(Path rootPath) { this.storageFilePath = rootPath.resolve(SERVER_INFO_PB); } public void store(ServerStatusInfo serverStatus, Set features, Map globalSettings) { FileUtils.mkdirs(storageFilePath.getParent()); var serverInfoToStore = adapt(serverStatus, features, globalSettings); LOG.debug("Storing server info in {}", storageFilePath); rwLock.write(() -> writeToFile(serverInfoToStore, storageFilePath)); LOG.debug("Stored server info"); } public Optional read() { return rwLock.read(() -> Files.exists(storageFilePath) ? Optional.of(adapt(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.ServerInfo.parser()))) : Optional.empty()); } private static Sonarlint.ServerInfo adapt(ServerStatusInfo serverStatus, Set features, Map globalSettings) { return Sonarlint.ServerInfo.newBuilder() .setVersion(serverStatus.version()) .setServerId(serverStatus.id()) .putAllGlobalSettings(globalSettings) .addAllSupportedFeatures(features.stream().map(Feature::getKey).toList()) .build(); } private static StoredServerInfo adapt(Sonarlint.ServerInfo serverInfo) { var globalSettings = serverInfo.getGlobalSettingsMap(); if (globalSettings.isEmpty()) { // migration for not yet synchronized storage globalSettings = new HashMap<>(); if (serverInfo.hasIsMqrMode()) { globalSettings.put(MQR_MODE_SETTING, Boolean.toString(serverInfo.getIsMqrMode())); } globalSettings.put(ServerSettings.EARLY_ACCESS_MISRA_ENABLED, Boolean.toString(serverInfo.getMisraEarlyAccessRulesEnabled())); } // Making sure that CFamily analyzer gets updated flag even with old Server/Cloud. if (globalSettings.containsKey(ServerSettings.EARLY_ACCESS_MISRA_ENABLED)) { globalSettings = new HashMap<>(globalSettings); globalSettings.put(ServerSettings.MISRA_COMPLIANCE_ENABLED, globalSettings.get(ServerSettings.EARLY_ACCESS_MISRA_ENABLED)); } return new StoredServerInfo(Version.create(serverInfo.getVersion()), serverInfo.getSupportedFeaturesList().stream().map(Feature::fromKey).flatMap(Optional::stream).collect(Collectors.toSet()), new ServerSettings(globalSettings), serverInfo.getServerId()); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerIssueStoresManager.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; public class ServerIssueStoresManager { private final Map serverIssueStoreByKey = new ConcurrentHashMap<>(); private final SonarLintDatabase database; private final String connectionId; public ServerIssueStoresManager(String connectionId, SonarLintDatabase database) { this.database = database; this.connectionId = connectionId; } public ProjectServerIssueStore get(String projectKey) { return serverIssueStoreByKey.computeIfAbsent(projectKey, p -> new ServerFindingRepository(database.dsl(), connectionId, projectKey)); } public void delete() { // removing connections via any repository is fine, it's the same tables behind serverIssueStoreByKey.values().stream().findFirst() .ifPresent(repository -> repository.removeFindingsForConnection(connectionId)); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/SmartNotificationsStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class SmartNotificationsStorage { private static final SonarLintLogger LOG = SonarLintLogger.get(); public static final String LAST_EVENT_POLLING_PB = "last_event_polling.pb"; private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public SmartNotificationsStorage(Path projectStorageRoot) { this.storageFilePath = projectStorageRoot.resolve(LAST_EVENT_POLLING_PB); } public void store(Long lastEventPolling) { FileUtils.mkdirs(storageFilePath.getParent()); var serverInfoToStore = adapt(lastEventPolling); LOG.debug("Storing last event polling in {}", storageFilePath); rwLock.write(() -> writeToFile(serverInfoToStore, storageFilePath)); } public Optional readLastEventPolling() { try { return rwLock.read(() -> Files.exists(storageFilePath) ? Optional.of(adapt(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.LastEventPolling.parser()))) : Optional.empty()); } catch (StorageException e) { LOG.debug("Couldn't access storage to read and update last event polling: " + storageFilePath); return Optional.empty(); } } private static Sonarlint.LastEventPolling adapt(Long lastEventPolling) { return Sonarlint.LastEventPolling.newBuilder().setLastEventPolling(lastEventPolling).build(); } private static Long adapt(Sonarlint.LastEventPolling lastEventPolling) { return lastEventPolling.getLastEventPolling(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/StorageException.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import org.sonarsource.sonarlint.core.commons.SonarLintException; public class StorageException extends SonarLintException { public StorageException(String msg) { super(msg); } public StorageException(String msg, Throwable cause) { super(msg, cause); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/StorageUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; public class StorageUtils { public static Set deserializeLanguages(Optional lastEnabledLanguages) { Set lastIssueEnabledLanguagesStringSet = Collections.emptySet(); Set lastIssueEnabledLanguagesSet = new HashSet<>(); if (lastEnabledLanguages.isPresent()) { lastIssueEnabledLanguagesStringSet = Stream.of(lastEnabledLanguages.get().split(",", -1)) .collect(Collectors.toSet()); } for(String languageString : lastIssueEnabledLanguagesStringSet){ var language = SonarLanguage.getLanguageByLanguageKey(languageString); if(language.isPresent()){ lastIssueEnabledLanguagesSet.add(language.get()); } } return lastIssueEnabledLanguagesSet; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/TarGzUtils.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.BufferedInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.io.IOUtils; public class TarGzUtils { private TarGzUtils() { } public static void extractTarGz(Path tarGzFile, Path destinationDir) throws IOException { try (var fi = Files.newInputStream(tarGzFile); var bi = new BufferedInputStream(fi); var gzi = new GzipCompressorInputStream(bi); var o = new TarArchiveInputStream(gzi)) { ArchiveEntry entry = null; while ((entry = o.getNextEntry()) != null) { if (!o.canReadEntryData(entry)) { throw new IllegalStateException("Unable to read entry data from " + tarGzFile); } Path f = fileName(destinationDir, entry); if (entry.isDirectory()) { Files.createDirectories(f); } else { Path parent = f.getParent(); Files.createDirectories(parent); try (var os = Files.newOutputStream(f)) { IOUtils.copy(o, os); } } } } } private static Path fileName(Path destinationDir, ArchiveEntry zipEntry) throws IOException { var destFile = destinationDir.resolve(zipEntry.getName()); if (!destFile.normalize().startsWith(destinationDir)) { throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); } return destFile; } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/UpdateSummary.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.util.List; import java.util.Set; import java.util.UUID; public record UpdateSummary(Set deletedItemIds, List addedItems, List updatedItems) { public boolean hasAnythingChanged() { return !deletedItemIds.isEmpty() || !addedItems.isEmpty() || !updatedItems.isEmpty(); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/UserStorage.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.serverconnection.FileUtils; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil.writeToFile; public class UserStorage { public static final String USER_PB = "user.pb"; private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Path storageFilePath; private final RWLock rwLock = new RWLock(); public UserStorage(Path rootPath) { this.storageFilePath = rootPath.resolve(USER_PB); } public void store(String userId) { FileUtils.mkdirs(storageFilePath.getParent()); var user = Sonarlint.User.newBuilder().setId(userId).build(); LOG.debug("Storing user in {}", storageFilePath); rwLock.write(() -> writeToFile(user, storageFilePath)); LOG.debug("Stored user"); } public Optional read() { return rwLock.read(() -> Files.exists(storageFilePath) ? Optional.of(ProtobufFileUtil.readFile(storageFilePath, Sonarlint.User.parser()).getId()) : Optional.empty()); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/UuidBinding.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.io.ByteArrayInputStream; import java.util.UUID; import jetbrains.exodus.bindings.BindingUtils; import jetbrains.exodus.bindings.ComparableBinding; import jetbrains.exodus.util.LightOutputStream; import org.jetbrains.annotations.NotNull; public class UuidBinding extends ComparableBinding { @Override public Comparable readObject(@NotNull ByteArrayInputStream stream) { return UUID.fromString(BindingUtils.readString(stream)); } @Override public void writeObject(@NotNull LightOutputStream output, @NotNull Comparable object) { final var uuid = (UUID) object; output.writeString(uuid.toString()); } } ================================================ FILE: backend/server-connection/src/main/java/org/sonarsource/sonarlint/core/serverconnection/storage/package-info.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.serverconnection.storage; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/server-connection/src/main/proto/sonarlint.proto ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ syntax = "proto3"; package sonarlint; // The java package can be changed without breaking compatibility. // it impacts only the generated Java code. option java_package = "org.sonarsource.sonarlint.core.serverconnection.proto"; option optimize_for = SPEED; message ServerInfo { string version = 1; // deprecated, store in the settings map below optional bool isMqrMode = 2 [deprecated=true]; // deprecated, store in the settings map below optional bool misraEarlyAccessRulesEnabled = 3 [deprecated=true]; map globalSettings = 4; repeated string supportedFeatures = 5; string serverId = 6; } message PluginReferences { map plugins_by_key = 1; message PluginReference { string key = 1; string hash = 2; string filename = 3; } } message AnalyzerConfiguration { map settings = 1; map rule_sets_by_language_key = 2; uint32 schema_version = 3; } message RuleSet { repeated ActiveRule rule = 1; string last_modified = 3; message ActiveRule { string rule_key = 1; string severity = 2; string template_key = 3; map params = 4; repeated Impact overriddenImpacts = 5; } } message ProjectBranches { repeated string branch_name = 1; string main_branch_name = 2; } message Flows { repeated Flow flow = 1; } message Flow { repeated Location location = 1; } message Impact { string softwareQuality = 1; string severity = 2; } message TextRange { int32 start_line = 1; int32 start_line_offset = 2; int32 end_line = 3; int32 end_line_offset = 4; string hash = 5; } message Location { optional string file_path = 1; string message = 2; optional TextRange text_range = 3; } message LastEventPolling { int64 last_event_polling = 1; } enum NewCodeDefinitionMode { UNKNOWN = 0; NUMBER_OF_DAYS = 1; PREVIOUS_VERSION = 2; REFERENCE_BRANCH = 3; SPECIFIC_ANALYSIS = 4; } message NewCodeDefinition { NewCodeDefinitionMode mode = 1; optional int32 days = 2; optional int64 thresholdDate = 3; optional string version = 4; optional string referenceBranch = 5; } message Organization { string id = 1; string uuidV4 = 2; } message User { optional string id = 1; } message AiCodeFixSettings { repeated string supportedRules = 1; bool organizationEligible = 2; AiCodeFixEnablement enablement = 3; repeated string enabledProjectKeys = 4; } enum AiCodeFixEnablement { UNKNOWN_ENABLEMENT = 0; DISABLED = 1; ENABLED_FOR_SOME_PROJECTS = 2; ENABLED_FOR_ALL_PROJECTS = 3; } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/AnalyzerConfigurationStorageTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; class AnalyzerConfigurationStorageTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @Test void should_consider_config_storage_invalid_if_not_readable_and_do_not_log_exception(@TempDir Path tempDir) { var analyzerConfigurationStorage = new AnalyzerConfigurationStorage(tempDir); var valid = analyzerConfigurationStorage.isValid(); assertFalse(valid); assertThat(logTester.logs()).contains("Analyzer configuration storage doesn't exist: " + tempDir.toAbsolutePath().resolve("analyzer_config.pb")); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/DownloadExceptionTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.IOException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class DownloadExceptionTests { @Test void testNoArgs() { var exception = new DownloadException(); assertThat(exception.getMessage()).isNull(); assertThat(exception.getCause()).isNull(); } @Test void testCauseAndMsg() { var cause = new IOException("cause msg"); var exception = new DownloadException("msg", cause); assertThat(exception.getMessage()).isEqualTo("msg"); assertThat(exception.getCause()).isEqualTo(cause); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/FileUtilsTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.File; import java.io.IOException; import java.nio.file.AccessDeniedException; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; class FileUtilsTests { @Test void deleteRecursively(@TempDir Path dir) { var fileInDir = createNewFile(dir, "dummy"); assertThat(fileInDir).isFile(); FileUtils.deleteRecursively(dir); assertThat(fileInDir).doesNotExist(); assertThat(dir).doesNotExist(); } @Test void deleteRecursively_should_ignore_nonexistent_dir(@TempDir Path temp) { var dir = new File(temp.toFile(), "nonexistent"); assertThat(dir).doesNotExist(); FileUtils.deleteRecursively(dir.toPath()); } @Test void deleteRecursively_should_delete_file(@TempDir Path temp) { var file = createNewFile(temp, "foo.txt"); assertThat(file).isFile(); FileUtils.deleteRecursively(file.toPath()); assertThat(file).doesNotExist(); } @Test void deleteRecursively_should_delete_deeply_nested_dirs(@TempDir Path basedir) { var deeplyNestedDir = basedir.resolve("a").resolve("b").resolve("c"); assertThat(deeplyNestedDir.toFile().isDirectory()).isFalse(); FileUtils.mkdirs(deeplyNestedDir); FileUtils.deleteRecursively(basedir); assertThat(basedir.toFile()).doesNotExist(); } @Test void mkdirs(@TempDir Path temp) { var deeplyNestedDir = temp.resolve("a").resolve("b").resolve("c"); assertThat(deeplyNestedDir).doesNotExist(); if (deeplyNestedDir.toFile().mkdir()) { throw new IllegalStateException("creating nested dir should have failed"); } FileUtils.mkdirs(deeplyNestedDir); assertThat(deeplyNestedDir).isDirectory(); } @Test void mkdirs_should_fail_if_destination_is_a_file(@TempDir Path temp) { var file = createNewFile(temp, "foo").toPath(); assertThrows(IllegalStateException.class, () -> FileUtils.mkdirs(file)); } @Test void always_retry_at_least_once() throws IOException { var runnable = mock(FileUtils.IORunnable.class); FileUtils.retry(runnable, 0); verify(runnable, times(1)).run(); } @Test void retry_on_failure() throws IOException { int[] count = {0}; FileUtils.IORunnable throwOnce = () -> { count[0]++; if (count[0] == 1) { throw new AccessDeniedException("foo"); } }; FileUtils.retry(throwOnce, 10); assertThat(count[0]).isEqualTo(2); } private File createNewFile(Path basedir, String filename) { var path = basedir.resolve(filename); try { return Files.createFile(path).toFile(); } catch (IOException e) { fail("could not create file: " + path); } throw new IllegalStateException("should be unreachable"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/HotspotDownloaderTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.time.Instant; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Hotspots; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; class HotspotDownloaderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String DUMMY_KEY = "dummyKey"; @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private ServerApi serverApi; private HotspotDownloader underTest; @BeforeEach void prepare() { underTest = new HotspotDownloader(Set.of(SonarLanguage.JAVA)); serverApi = new ServerApi(mockServer.serverApiHelper()); } @Test void test_download_one_hotspot_pull_ws() { var timestamp = Hotspots.HotspotPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var hotspot1 = Hotspots.HotspotLite.newBuilder() .setKey("someHotspotKey") .setFilePath("foo/bar/Hello.java") .setVulnerabilityProbability(VulnerabilityProbability.LOW.toString()) .setStatus("TO_REVIEW") .setMessage("This is security sensitive") .setCreationDate(123456789L) .setTextRange(Hotspots.TextRange.newBuilder() .setStartLine(1) .setStartLineOffset(2) .setEndLine(3) .setEndLineOffset(4) .setHash("clearly not a hash") .build()) .setRuleKey("java:S123") .setClosed(false) .build(); var hotspot2 = Hotspots.HotspotLite.newBuilder() .setKey("otherHotspotKey") .setFilePath("foo/bar/Hello.java") .setVulnerabilityProbability(VulnerabilityProbability.LOW.toString()) .setStatus("REVIEWED") .setResolution("SAFE") .setMessage("This is security sensitive") .setCreationDate(123456789L) .setTextRange(Hotspots.TextRange.newBuilder() .setStartLine(5) .setStartLineOffset(6) .setEndLine(7) .setEndLineOffset(8) .setHash("not a hash either") .build()) .setRuleKey("java:S123") .setClosed(false) .build(); mockServer.addProtobufResponseDelimited("/api/hotspots/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, hotspot1, hotspot2); var result = underTest.downloadFromPull(serverApi.hotspot(), DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getChangedHotspots()).hasSize(2); assertThat(result.getClosedHotspotKeys()).isEmpty(); var serverHotspot1 = result.getChangedHotspots().get(0); assertThat(serverHotspot1.getKey()).isEqualTo("someHotspotKey"); assertThat(serverHotspot1.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(serverHotspot1.getVulnerabilityProbability()).isEqualTo(VulnerabilityProbability.LOW); assertThat(serverHotspot1.getStatus()).isEqualTo(HotspotReviewStatus.TO_REVIEW); assertThat(serverHotspot1.getMessage()).isEqualTo("This is security sensitive"); assertThat(serverHotspot1.getCreationDate()).isAfter(Instant.EPOCH); assertThat(serverHotspot1.getTextRange().getStartLine()).isEqualTo(1); assertThat(serverHotspot1.getTextRange().getStartLineOffset()).isEqualTo(2); assertThat(serverHotspot1.getTextRange().getEndLine()).isEqualTo(3); assertThat(serverHotspot1.getTextRange().getEndLineOffset()).isEqualTo(4); assertThat(((TextRangeWithHash) serverHotspot1.getTextRange()).getHash()).isEqualTo("clearly not a hash"); assertThat(serverHotspot1.getRuleKey()).isEqualTo("java:S123"); var serverHotspot2 = result.getChangedHotspots().get(1); assertThat(serverHotspot2.getKey()).isEqualTo("otherHotspotKey"); assertThat(serverHotspot2.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(serverHotspot2.getVulnerabilityProbability()).isEqualTo(VulnerabilityProbability.LOW); assertThat(serverHotspot2.getStatus()).isEqualTo(HotspotReviewStatus.SAFE); assertThat(serverHotspot2.getMessage()).isEqualTo("This is security sensitive"); assertThat(serverHotspot2.getCreationDate()).isAfter(Instant.EPOCH); assertThat(serverHotspot2.getTextRange().getStartLine()).isEqualTo(5); assertThat(serverHotspot2.getTextRange().getStartLineOffset()).isEqualTo(6); assertThat(serverHotspot2.getTextRange().getEndLine()).isEqualTo(7); assertThat(serverHotspot2.getTextRange().getEndLineOffset()).isEqualTo(8); assertThat(((TextRangeWithHash) serverHotspot2.getTextRange()).getHash()).isEqualTo("not a hash either"); assertThat(serverHotspot2.getRuleKey()).isEqualTo("java:S123"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/IssueDownloaderTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.Set; import mockwebserver3.MockResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.scanner.protocol.Constants.Severity; import org.sonar.scanner.protocol.input.ScannerInput; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.ServerErrorException; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.IssueLite; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.Location; import org.sonarsource.sonarlint.core.serverconnection.issues.FileLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class IssueDownloaderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String DUMMY_KEY = "dummyKey"; @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private ServerApi serverApi; private IssueDownloader underTest; @BeforeEach void prepare() { underTest = new IssueDownloader(Set.of(SonarLanguage.JAVA)); serverApi = new ServerApi(mockServer.serverApiHelper()); } @Test void test_download_one_issue_old_batch_ws() { var response = ScannerInput.ServerIssue.newBuilder() .setKey("uuid") .setRuleRepository("sonarjava") .setRuleKey("S123") .setChecksum("hash") .setMsg("Primary message") .setLine(1) .setCreationDate(123456789L) .setPath("foo/bar/Hello.java") .setType("BUG") .setManualSeverity(false) .setSeverity(Severity.BLOCKER) .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY, response); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); var serverIssue = issues.get(0); assertThat(serverIssue).isInstanceOf(LineLevelServerIssue.class); assertThat(serverIssue.getKey()).isEqualTo("uuid"); assertThat(serverIssue.getType()).isEqualTo(RuleType.BUG); assertThat(serverIssue.getUserSeverity()).isNull(); assertThat(((LineLevelServerIssue) serverIssue).getLineHash()).isEqualTo("hash"); assertThat(serverIssue.getMessage()).isEqualTo("Primary message"); assertThat(serverIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(((LineLevelServerIssue) serverIssue).getLine()).isEqualTo(1); } @Test void test_download_one_issue_old_batch_ws_with_user_severity() { var response = ScannerInput.ServerIssue.newBuilder() .setKey("uuid") .setRuleRepository("sonarjava") .setRuleKey("S123") .setChecksum("hash") .setMsg("Primary message") .setLine(1) .setCreationDate(123456789L) .setPath("foo/bar/Hello.java") .setType("BUG") .setManualSeverity(true) .setSeverity(Severity.BLOCKER) .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY, response); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); var serverIssue = issues.get(0); assertThat(serverIssue.getUserSeverity()).isEqualTo(IssueSeverity.BLOCKER); } @Test void test_download_one_file_level_issue_old_batch_ws() { var response = ScannerInput.ServerIssue.newBuilder() .setKey("uuid") .setRuleRepository("sonarjava") .setRuleKey("S123") .setMsg("Primary message") .setCreationDate(123456789L) .setPath("foo/bar/Hello.java") .setType("BUG") .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY, response); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); var serverIssue = issues.get(0); assertThat(serverIssue).isInstanceOf(FileLevelServerIssue.class); assertThat(serverIssue.getKey()).isEqualTo("uuid"); assertThat(serverIssue.getMessage()).isEqualTo("Primary message"); assertThat(serverIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); } @Test void test_download_one_issue_pull_ws() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("uuid") .setRuleKey("sonarjava:S123") .setType(Common.RuleType.BUG) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message") .setTextRange(org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.TextRange.newBuilder().setStartLine(1).setStartLineOffset(2).setEndLine(3) .setEndLineOffset(4).setHash("hash"))) .setCreationDate(123456789L) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, issue); var result = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getChangedIssues()).hasSize(1); assertThat(result.getClosedIssueKeys()).isEmpty(); var serverIssue = result.getChangedIssues().get(0); assertThat(serverIssue).isInstanceOf(RangeLevelServerIssue.class); assertThat(serverIssue.getKey()).isEqualTo("uuid"); assertThat(serverIssue.getMessage()).isEqualTo("Primary message"); assertThat(serverIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(serverIssue.getUserSeverity()).isNull(); assertThat(serverIssue.getType()).isEqualTo(RuleType.BUG); assertThat(((RangeLevelServerIssue) serverIssue).getTextRange().getStartLine()).isEqualTo(1); assertThat(((RangeLevelServerIssue) serverIssue).getTextRange().getStartLineOffset()).isEqualTo(2); assertThat(((RangeLevelServerIssue) serverIssue).getTextRange().getEndLine()).isEqualTo(3); assertThat(((RangeLevelServerIssue) serverIssue).getTextRange().getEndLineOffset()).isEqualTo(4); assertThat(((RangeLevelServerIssue) serverIssue).getTextRange().getHash()).isEqualTo("hash"); } @Test void test_download_one_issue_pull_ws_with_user_severity() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("uuid") .setRuleKey("sonarjava:S123") .setType(Common.RuleType.BUG) .setUserSeverity(Common.Severity.MAJOR) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message") .setTextRange(org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.TextRange.newBuilder().setStartLine(1).setStartLineOffset(2).setEndLine(3) .setEndLineOffset(4).setHash("hash"))) .setCreationDate(123456789L) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, issue); var result = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getChangedIssues()).hasSize(1); assertThat(result.getClosedIssueKeys()).isEmpty(); var serverIssue = result.getChangedIssues().get(0); assertThat(serverIssue.getUserSeverity()).isEqualTo(IssueSeverity.MAJOR); } @Test void test_download_one_issue_pull_ws_with_user_impacts() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("uuid") .setRuleKey("sonarjava:S123") .setType(Common.RuleType.BUG) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message") .setTextRange(Issues.TextRange.newBuilder().setStartLine(1).setStartLineOffset(2).setEndLine(3) .setEndLineOffset(4).setHash("hash"))) .setCreationDate(123456789L) .addImpacts(Common.Impact.newBuilder() .setSoftwareQuality(Common.SoftwareQuality.SECURITY) .setSeverity(Common.ImpactSeverity.HIGH) .build()) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, issue); var result = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getChangedIssues()).hasSize(1); assertThat(result.getClosedIssueKeys()).isEmpty(); var serverIssue = result.getChangedIssues().get(0); assertThat(serverIssue.getImpacts()).isEqualTo(Map.of(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)); } @Test void test_download_one_file_level_issue_pull_ws() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("uuid") .setRuleKey("sonarjava:S123") .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message")) .setCreationDate(123456789L) .setType(Common.RuleType.BUG) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, issue); var result = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getChangedIssues()).hasSize(1); assertThat(result.getClosedIssueKeys()).isEmpty(); var serverIssue = result.getChangedIssues().get(0); assertThat(serverIssue).isInstanceOf(FileLevelServerIssue.class); assertThat(serverIssue.getKey()).isEqualTo("uuid"); assertThat(serverIssue.getMessage()).isEqualTo("Primary message"); assertThat(serverIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(serverIssue.getType()).isEqualTo(RuleType.BUG); } @Test void test_download_closed_file_level_issues_from_pull_ws() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("key") .setClosed(true) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message") .setTextRange(org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.TextRange.newBuilder().setStartLine(1).setStartLineOffset(2).setEndLine(3) .setEndLineOffset(4).setHash("hash"))) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java&changedSince=123456789", timestamp, issue); var result = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.of(Instant.ofEpochMilli(123456789)), new SonarLintCancelMonitor()); assertThat(result.getChangedIssues()).isEmpty(); assertThat(result.getClosedIssueKeys()).containsOnly("key"); } @Test void test_download_issue_ignore_project_level() { var response = ScannerInput.ServerIssue.newBuilder() .setRuleRepository("sonarjava") .setRuleKey("S123") .setChecksum("hash") .setMsg("Primary message") .setLine(1) .setCreationDate(123456789L) // No path .setModuleKey("project") .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY, response); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).isEmpty(); } @Test void test_pull_issue_ignore_project_level() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var issue = IssueLite.newBuilder() .setKey("uuid") .setRuleKey("sonarjava:S123") .setMainLocation(Location.newBuilder().setMessage("Primary message")) .setCreationDate(123456789L) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, issue); var issues = underTest.downloadFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(issues.getChangedIssues()).isEmpty(); assertThat(issues.getClosedIssueKeys()).isEmpty(); } @Test void test_ignore_taint_vulnerabilities() { var issue1 = ScannerInput.ServerIssue.newBuilder() .setRuleRepository("sonarjava") .setRuleKey("S123") .setChecksum("hash1") .setMsg("Primary message 1") .setLine(1) .setCreationDate(123456789L) .setPath("foo/bar/Hello.java") .setModuleKey("project") .setType("BUG") .build(); var taint1 = ScannerInput.ServerIssue.newBuilder() .setRuleRepository("javasecurity") .setRuleKey("S789") .setChecksum("hash2") .setMsg("Primary message 2") .setLine(2) .setCreationDate(123456789L) .setPath("foo/bar/Hello2.java") .setModuleKey("project") .setType("VULNERABILITY") .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY, issue1, taint1); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); } @Test void test_download_no_issues() { mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).isEmpty(); } @Test void test_fail_other_codes() { mockServer.addResponse("/batch/issues?key=" + DUMMY_KEY, new MockResponse.Builder().code(503).build()); var cancelMonitor = new SonarLintCancelMonitor(); var thrown = assertThrows(ServerErrorException.class, () -> underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, cancelMonitor)); assertThat(thrown).hasMessageContaining("Error 503"); } @Test void test_return_empty_if_404() { mockServer.addResponse("/batch/issues?key=" + DUMMY_KEY, new MockResponse.Builder().code(404).build()); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).isEmpty(); } @Test void test_filter_batch_issues_by_branch_if_branch_parameter_provided() { var response = ScannerInput.ServerIssue.newBuilder() .setRuleRepository("sonarjava") .setRuleKey("S123") .setPath("src/Foo.java") .setType("BUG") .build(); mockServer.addProtobufResponseDelimited("/batch/issues?key=" + DUMMY_KEY + "&branch=branchName", response); var issues = underTest.downloadFromBatch(serverApi, DUMMY_KEY, "branchName", new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/IssueStorePathsTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class IssueStorePathsTests { @Test void local_path_to_sq_path_uses_both_prefixes() { var projectBinding = new ProjectBinding("project", "sq", "ide"); var sqPath = IssueStorePaths.idePathToServerPath(projectBinding, Path.of("ide/project1/path1")); assertThat(sqPath).isEqualTo(Path.of("sq/project1/path1")); } @Test void local_path_to_fileKey() { var projectBinding = new ProjectBinding("projectKey", "project", "ide"); var fileKey = IssueStorePaths.idePathToFileKey(projectBinding, Paths.get("ide/B/path1")); assertThat(fileKey).isEqualTo("projectKey:project/B/path1"); } @Test void local_path_to_sq_path_returns_null_if_path_doesnt_match_prefix() { var projectBinding = new ProjectBinding("project", "sq", "ide"); var sqPath = IssueStorePaths.idePathToServerPath(projectBinding, Path.of("unknown/project1/path1").normalize()); assertThat(sqPath).isNull(); } @Test void local_path_to_sq_path_returns_null_if_path_match_prefix_partially() { var projectBinding = new ProjectBinding("project", "sq", "src"); var sqPath = IssueStorePaths.idePathToServerPath(projectBinding, Path.of("src2/project1/path1")); assertThat(sqPath).isNull(); } @Test void local_path_to_sq_path_without_sq_prefix() { var projectBinding = new ProjectBinding("project", "", "ide"); var sqPath = IssueStorePaths.idePathToServerPath(projectBinding, Path.of("ide/project1/path1")); assertThat(sqPath).isEqualTo(Path.of("project1/path1")); } @Test void local_path_to_sq_path_without_ide_prefix() { var projectBinding = new ProjectBinding("project", "sq", ""); var sqPath = IssueStorePaths.idePathToServerPath(projectBinding, Path.of("ide/project1/path1")); assertThat(sqPath).isEqualTo(Path.of("sq/ide/project1/path1")); } @Test void local_path_to_fileKey_returns_null_if_path_doesnt_match_prefix() { var projectBinding = new ProjectBinding("project", "project", "ide"); var fileKey = IssueStorePaths.idePathToFileKey(projectBinding, Path.of("unknown/B/path1")); assertThat(fileKey).isNull(); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ProjectBindingTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ProjectBindingTests { @Test void should_assign_all_parameters_in_constructor() { var projectBinding = new ProjectBinding("key", "sqPrefix", "localPrefix"); assertThat(projectBinding.projectKey()).isEqualTo("key"); assertThat(projectBinding.serverPathPrefix()).isEqualTo("sqPrefix"); assertThat(projectBinding.idePathPrefix()).isEqualTo("localPrefix"); } @Test void equals_and_hashCode_should_use_all_fields() { var projectBinding1 = new ProjectBinding("key", "sqPrefix", "localPrefix"); var projectBinding2 = new ProjectBinding("key1", "sqPrefix", "localPrefix"); var projectBinding3 = new ProjectBinding("key", "sqPrefix1", "localPrefix"); var projectBinding4 = new ProjectBinding("key", "sqPrefix", "localPrefix1"); var projectBinding5 = new ProjectBinding("key", "sqPrefix", "localPrefix"); assertThat(projectBinding1.equals(projectBinding2)).isFalse(); assertThat(projectBinding1.equals(projectBinding3)).isFalse(); assertThat(projectBinding1.equals(projectBinding4)).isFalse(); assertThat(projectBinding1.equals(projectBinding5)).isTrue(); assertThat(projectBinding1.hashCode()).isNotEqualTo(projectBinding2.hashCode()); assertThat(projectBinding1.hashCode()).isNotEqualTo(projectBinding3.hashCode()); assertThat(projectBinding1.hashCode()).isNotEqualTo(projectBinding4.hashCode()); assertThat(projectBinding1.hashCode()).isEqualTo(projectBinding5.hashCode()); } @Test void serverPathToIdePath_no_match_from_server_path() { var projectBinding = new ProjectBinding("key", "sqPrefix", "localPrefix"); assertThat(projectBinding.serverPathToIdePath("notSqPrefix/some/path")).isEmpty(); } @Test void serverPathToIdePath_general_case() { var projectBinding = new ProjectBinding("key", "sq/path/prefix", "local/prefix"); assertThat(projectBinding.serverPathToIdePath("sq/path/prefix/some/path")).hasValue("local/prefix/some/path"); } @Test void serverPathToIdePath_empty_local_path() { var projectBinding = new ProjectBinding("key", "sq/path/prefix", ""); assertThat(projectBinding.serverPathToIdePath("sq/path/prefix/some/path")).hasValue("some/path"); } @Test void serverPathToIdePath_empty_sq_path() { var projectBinding = new ProjectBinding("key", "", "local/prefix"); assertThat(projectBinding.serverPathToIdePath("some/path")).hasValue("local/prefix/some/path"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ProjectStoragePathsTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.junit.jupiter.api.Test; import static org.apache.commons.lang3.StringUtils.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs; class ProjectStoragePathsTests { @Test void encode_paths_for_fs() { assertThat(encodeForFs("my/string%to encode**")).isEqualTo("6d792f737472696e6725746f20656e636f64652a2a"); assertThat(encodeForFs("AU-TpxcA-iU5OvuD2FLz").toLowerCase()).isNotEqualTo(encodeForFs("AU-TpxcA-iU5OvuD2FLZ")); assertThat(encodeForFs("too_long_for_most_fs" + repeat("a", 1000))).hasSize(255); assertThat(encodeForFs("too_long_for_most_fs" + repeat("a", 1000))) .isNotEqualTo(encodeForFs("too_long_for_most_fs" + repeat("a", 1000) + "2")); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ServerHotspotUpdaterTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.time.Instant; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.hotspot.HotspotApi; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.storage.ProjectServerIssueStore; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.serverconnection.storage.ServerHotspotFixtures.aServerHotspot; class ServerHotspotUpdaterTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String PROJECT_KEY = "module"; private final ProjectServerIssueStore issueStore = mock(ProjectServerIssueStore.class); private final ProjectBinding projectBinding = new ProjectBinding(PROJECT_KEY, "", ""); private ServerHotspotUpdater updater; private HotspotApi hotspotApi; private HotspotDownloader hotspotDownloader; @BeforeEach void setUp() { hotspotApi = mock(HotspotApi.class); hotspotDownloader = mock(HotspotDownloader.class); ConnectionStorage storage = mock(ConnectionStorage.class); var projectStorage = mock(SonarProjectStorage.class); when(storage.project(PROJECT_KEY)).thenReturn(projectStorage); when(projectStorage.findings()).thenReturn(issueStore); updater = new ServerHotspotUpdater(storage, hotspotDownloader); } @Test void should_sync_hotspots() { var timestamp = Instant.ofEpochMilli(123456789L); var hotspotKey = "hotspotKey"; var hotspots = List.of(aServerHotspot(hotspotKey)); var cancelMonitor = new SonarLintCancelMonitor(); when(hotspotDownloader.downloadFromPull(hotspotApi, PROJECT_KEY, "branch", Optional.empty(), cancelMonitor)) .thenReturn(new HotspotDownloader.PullResult(timestamp, hotspots, Set.of())); updater.sync(hotspotApi, PROJECT_KEY, "branch", Set.of(SonarLanguage.C), cancelMonitor); var hotspotCaptor = ArgumentCaptor.forClass(List.class); verify(issueStore).mergeHotspots(eq("branch"), hotspotCaptor.capture(), eq(Set.of()), eq(timestamp), eq(Set.of(SonarLanguage.C))); assertThat(hotspotCaptor.getValue()).hasSize(1); var capturedHotspot = (ServerHotspot) (hotspotCaptor.getValue().get(0)); assertThat(capturedHotspot.getKey()).isEqualTo(hotspotKey); } @Test void update_hotspots_with_pull_when_enabled_language_not_changed() { var timestamp = Instant.ofEpochMilli(123456789L); var lastHotspotEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); var hotspotKey = "hotspotKey"; var hotspots = List.of(aServerHotspot(hotspotKey)); var cancelMonitor = new SonarLintCancelMonitor(); when(hotspotDownloader.downloadFromPull(hotspotApi, PROJECT_KEY, "branch", Optional.of(timestamp), cancelMonitor)) .thenReturn(new HotspotDownloader.PullResult(timestamp, hotspots, Set.of())); when(issueStore.getLastHotspotEnabledLanguages("branch")).thenReturn(lastHotspotEnabledLanguages); when(issueStore.getLastHotspotSyncTimestamp("branch")).thenReturn(Optional.of(timestamp)); updater.sync(hotspotApi, PROJECT_KEY, "branch", Set.of(SonarLanguage.C, SonarLanguage.GO), cancelMonitor); var hotspotCaptor = ArgumentCaptor.forClass(List.class); verify(issueStore).mergeHotspots(eq("branch"), hotspotCaptor.capture(), eq(Set.of()), eq(timestamp), anySet()); assertThat(hotspotCaptor.getValue()).hasSize(1); var capturedHotspot = (ServerHotspot) (hotspotCaptor.getValue().get(0)); assertThat(capturedHotspot.getKey()).isEqualTo(hotspotKey); verify(hotspotDownloader).downloadFromPull(hotspotApi, projectBinding.projectKey(), "branch", Optional.of(timestamp), cancelMonitor); } @Test void update_hotspots_with_pull_when_enabled_language_changed() { var timestamp = Instant.ofEpochMilli(123456789L); var lastHotspotEnabledLanguages = Set.of(SonarLanguage.C); var hotspotKey = "hotspotKey"; var hotspots = List.of(aServerHotspot(hotspotKey)); var cancelMonitor = new SonarLintCancelMonitor(); when(hotspotDownloader.downloadFromPull(hotspotApi, PROJECT_KEY, "branch", Optional.empty(), cancelMonitor)) .thenReturn(new HotspotDownloader.PullResult(timestamp, hotspots, Set.of())); when(issueStore.getLastHotspotEnabledLanguages("branch")).thenReturn(lastHotspotEnabledLanguages); when(issueStore.getLastHotspotSyncTimestamp("branch")).thenReturn(Optional.of(timestamp)); updater.sync(hotspotApi, PROJECT_KEY, "branch", Set.of(SonarLanguage.C, SonarLanguage.GO), cancelMonitor); var hotspotCaptor = ArgumentCaptor.forClass(List.class); verify(issueStore).mergeHotspots(eq("branch"), hotspotCaptor.capture(), eq(Set.of()), eq(timestamp), anySet()); assertThat(hotspotCaptor.getValue()).hasSize(1); var capturedHotspot = (ServerHotspot) (hotspotCaptor.getValue().get(0)); assertThat(capturedHotspot.getKey()).isEqualTo(hotspotKey); verify(hotspotDownloader).downloadFromPull(hotspotApi, projectBinding.projectKey(), "branch", Optional.empty(), cancelMonitor); } @Test void update_hotspots_with_pull_when_last_enabled_language_were_not_there() { var timestamp = Instant.ofEpochMilli(123456789L); var lastHotspotEnabledLanguages = new HashSet(); var hotspotKey = "hotspotKey"; var hotspots = List.of(aServerHotspot(hotspotKey)); var cancelMonitor = new SonarLintCancelMonitor(); when(hotspotDownloader.downloadFromPull(hotspotApi, PROJECT_KEY, "branch", Optional.empty(), cancelMonitor)) .thenReturn(new HotspotDownloader.PullResult(timestamp, hotspots, Set.of())); when(issueStore.getLastHotspotEnabledLanguages("branch")).thenReturn(lastHotspotEnabledLanguages); when(issueStore.getLastHotspotSyncTimestamp("branch")).thenReturn(Optional.of(timestamp)); updater.sync(hotspotApi, PROJECT_KEY, "branch", Set.of(SonarLanguage.C, SonarLanguage.GO), cancelMonitor); var hotspotCaptor = ArgumentCaptor.forClass(List.class); verify(issueStore).mergeHotspots(eq("branch"), hotspotCaptor.capture(), eq(Set.of()), eq(timestamp), anySet()); assertThat(hotspotCaptor.getValue()).hasSize(1); var capturedHotspot = (ServerHotspot) (hotspotCaptor.getValue().get(0)); assertThat(capturedHotspot.getKey()).isEqualTo(hotspotKey); verify(hotspotDownloader).downloadFromPull(hotspotApi, projectBinding.projectKey(), "branch", Optional.empty(), cancelMonitor); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ServerInfoSynchronizerTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.Version; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.features.Feature; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Settings; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock; class ServerInfoSynchronizerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @TempDir Path tmpDir; private ServerInfoSynchronizer synchronizer; @BeforeEach void prepare() { var databaseService = mock(SonarLintDatabase.class); var storage = new ConnectionStorage(tmpDir, "connectionId", databaseService); synchronizer = new ServerInfoSynchronizer(storage); } @Test void it_should_read_version_from_storage_when_available() throws IOException { var connectionPath = tmpDir.resolve("636f6e6e656374696f6e4964"); Files.createDirectory(connectionPath); ProtobufFileUtil.writeToFile(Sonarlint.ServerInfo.newBuilder().setVersion("1.0.0").build(), connectionPath.resolve("server_info.pb")); var storedServerInfo = synchronizer.readOrSynchronizeServerInfo(new ServerApi(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth()), new SonarLintCancelMonitor()); assertThat(storedServerInfo) .extracting(StoredServerInfo::version) .hasToString("1.0.0"); } @Test void it_should_synchronize_version_and_settings() { mockServer.addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"9.9\",\"status\": \"UP\"}"); mockServer.addStringResponse("/api/features/list", "[\"sca\"]"); mockServer.addProtobufResponse("/api/settings/values.protobuf", Settings.ValuesWsResponse.newBuilder() .addSettings(Settings.Setting.newBuilder() .setKey("sonar.multi-quality-mode.enabled") .setValue("true")) .addSettings(Settings.Setting.newBuilder() .setKey("sonar.earlyAccess.misra.enabled") .setValue("true")) .build()); var storedServerInfo = synchronizer.readOrSynchronizeServerInfo(new ServerApi(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth()), new SonarLintCancelMonitor()); assertThat(storedServerInfo) .extracting(StoredServerInfo::version, StoredServerInfo::features, StoredServerInfo::globalSettings) .containsExactly(Version.create("9.9"), Set.of(Feature.SCA), new ServerSettings(Map.of( "sonar.multi-quality-mode.enabled", "true", "sonar.earlyAccess.misra.enabled", "true", "sonar.misracompliance.enabled", "true"))); } @Test void it_should_fail_when_server_is_down() { mockServer.addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"9.9\",\"status\": \"DOWN\"}"); var throwable = catchThrowable( () -> synchronizer.readOrSynchronizeServerInfo(new ServerApi(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth()), new SonarLintCancelMonitor())); assertThat(throwable) .isInstanceOf(IllegalStateException.class) .hasMessage("Server not ready (DOWN)"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ServerIssueUpdaterTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.nio.file.Path; import java.time.Instant; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import org.sonarsource.sonarlint.core.serverconnection.storage.ProjectServerIssueStore; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueFixtures.aServerIssue; import static org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueFixtures.aServerTaintIssue; class ServerIssueUpdaterTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String PROJECT_KEY = "module"; private final IssueDownloader downloader = mock(IssueDownloader.class); private final TaintIssueDownloader taintDownloader = mock(TaintIssueDownloader.class); private final ProjectServerIssueStore issueStore = mock(ProjectServerIssueStore.class); private ProjectBinding projectBinding = new ProjectBinding(PROJECT_KEY, "", ""); private ServerIssueUpdater updater; private ServerApi serverApi; @BeforeEach void setUp() { serverApi = new ServerApi(mock(ServerApiHelper.class)); ConnectionStorage storage = mock(ConnectionStorage.class); var projectStorage = mock(SonarProjectStorage.class); when(storage.project(PROJECT_KEY)).thenReturn(projectStorage); when(projectStorage.findings()).thenReturn(issueStore); updater = new ServerIssueUpdater(storage, downloader, taintDownloader); } @Test void update_project_issues_sonarcloud() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromBatch(serverApi, "module:file", null, cancelMonitor)).thenReturn(issues); when(serverApi.isSonarCloud()).thenReturn(true); updater.update(serverApi, projectBinding.projectKey(), "branch", Set.of(), cancelMonitor); verify(issueStore).replaceAllIssuesOfBranch(eq("branch"), anyList(), eq(Set.of())); } @Test void update_project_issues_with_pull_first_time() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.empty(); when(issueStore.getLastIssueSyncTimestamp("master")).thenReturn(lastSync); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor)).thenReturn(new IssueDownloader.PullResult(queryTimestamp, issues, Set.of())); updater.update(serverApi, projectBinding.projectKey(), "master", Set.of(), cancelMonitor); verify(issueStore).mergeIssues(eq("master"), anyList(), anySet(), eq(queryTimestamp), anySet()); } @Test void update_project_issues_with_pull_using_last_sync() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); when(issueStore.getLastIssueEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); when(issueStore.getLastIssueSyncTimestamp("master")).thenReturn(lastSync); when(downloader.getEnabledLanguages()).thenReturn(Set.of(SonarLanguage.C, SonarLanguage.GO)); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor)).thenReturn(new IssueDownloader.PullResult(queryTimestamp, issues, Set.of())); updater.update(serverApi, projectBinding.projectKey(), "master", Set.of(), cancelMonitor); verify(issueStore).mergeIssues(eq("master"), anyList(), anySet(), eq(queryTimestamp), anySet()); } @Test void update_project_issues_with_pull_when_there_were_no_enabled_languages() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = new HashSet(); when(issueStore.getLastIssueSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastIssueEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); when(downloader.getEnabledLanguages()).thenReturn(Set.of(SonarLanguage.C)); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor)).thenReturn(new IssueDownloader.PullResult(queryTimestamp, issues, Set.of())); updater.update(serverApi, projectBinding.projectKey(), "master", Set.of(), cancelMonitor); verify(downloader).downloadFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor); } @Test void update_project_issues_with_pull_when_enabled_language_changed() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); when(issueStore.getLastIssueSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastIssueEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); when(downloader.getEnabledLanguages()).thenReturn(Set.of(SonarLanguage.C)); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor)).thenReturn(new IssueDownloader.PullResult(queryTimestamp, issues, Set.of())); updater.update(serverApi, projectBinding.projectKey(), "master", Set.of(), cancelMonitor); verify(downloader).downloadFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor); } @Test void update_project_issues_with_pull_when_enabled_language_not_changed() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); when(issueStore.getLastIssueSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastIssueEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); when(downloader.getEnabledLanguages()).thenReturn(Set.of(SonarLanguage.C, SonarLanguage.GO)); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor)).thenReturn(new IssueDownloader.PullResult(queryTimestamp, issues, Set.of())); updater.update(serverApi, projectBinding.projectKey(), "master", Set.of(), cancelMonitor); verify(downloader).downloadFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor); } @Test void update_project_taints_with_pull_when_there_were_no_enabled_languages() { var issue = aServerTaintIssue(); List issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = new HashSet(); when(issueStore.getLastTaintSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastTaintEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); var cancelMonitor = new SonarLintCancelMonitor(); when(taintDownloader.downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor)).thenReturn(new TaintIssueDownloader.PullTaintResult(queryTimestamp, issues, Set.of())); updater.syncTaints(serverApi, projectBinding.projectKey(), "master", Set.of(SonarLanguage.C), cancelMonitor); verify(taintDownloader).downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor); } @Test void update_project_taints_with_pull_when_enabled_language_changed() { var issue = aServerTaintIssue(); List issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); when(issueStore.getLastTaintSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastTaintEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); var cancelMonitor = new SonarLintCancelMonitor(); when(taintDownloader.downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor)).thenReturn(new TaintIssueDownloader.PullTaintResult(queryTimestamp, issues, Set.of())); updater.syncTaints(serverApi, projectBinding.projectKey(), "master", Set.of(SonarLanguage.C), cancelMonitor); verify(taintDownloader).downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", Optional.empty(), cancelMonitor); } @Test void update_project_taints_with_pull_when_enabled_language_not_changed() { var issue = aServerTaintIssue(); List issues = Collections.singletonList(issue); var queryTimestamp = Instant.now(); var lastSync = Optional.of(Instant.ofEpochMilli(123456789)); var lastIssueEnabledLanguages = Set.of(SonarLanguage.C, SonarLanguage.GO); when(issueStore.getLastTaintSyncTimestamp("master")).thenReturn(lastSync); when(issueStore.getLastTaintEnabledLanguages("master")).thenReturn(lastIssueEnabledLanguages); var cancelMonitor = new SonarLintCancelMonitor(); when(taintDownloader.downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor)).thenReturn(new TaintIssueDownloader.PullTaintResult(queryTimestamp, issues, Set.of())); updater.syncTaints(serverApi, projectBinding.projectKey(), "master", Set.of(SonarLanguage.C, SonarLanguage.GO), cancelMonitor); verify(taintDownloader).downloadTaintFromPull(serverApi, projectBinding.projectKey(), "master", lastSync, cancelMonitor); } @Test void update_file_issues_for_unknown_file() { projectBinding = new ProjectBinding(PROJECT_KEY, "", "ide_prefix"); updater.updateFileIssuesIfNeeded(serverApi, PROJECT_KEY, Path.of("not_ide_prefix"), null, new SonarLintCancelMonitor()); verifyNoInteractions(downloader); verifyNoInteractions(issueStore); } @Test void error_downloading_file_issues() { var cancelMonitor = new SonarLintCancelMonitor(); when(serverApi.isSonarCloud()).thenReturn(true); when(downloader.downloadFromBatch(serverApi, "module:file", null, cancelMonitor)).thenThrow(IllegalArgumentException.class); var filePath = Path.of("file"); assertThrows(DownloadException.class, () -> updater.updateFileIssuesIfNeeded(serverApi, PROJECT_KEY, filePath, null, cancelMonitor)); } @Test void update_file_issues_sonarcloud() { var issue = aServerIssue(); List> issues = Collections.singletonList(issue); when(serverApi.isSonarCloud()).thenReturn(true); var cancelMonitor = new SonarLintCancelMonitor(); when(downloader.downloadFromBatch(serverApi, projectBinding.projectKey() + ":src/main/Foo.java", null, cancelMonitor)).thenReturn(issues); updater.updateFileIssuesIfNeeded(serverApi, PROJECT_KEY, Path.of("src/main/Foo.java"), "branch", cancelMonitor); verify(issueStore).replaceAllIssuesOfFile(eq("branch"), eq(Path.of("src/main/Foo.java")), anyList()); } @Test void dont_update_file_issues_with_pull() { updater.updateFileIssuesIfNeeded(serverApi, PROJECT_KEY, Path.of("src/main/Foo.java"), "branch", new SonarLintCancelMonitor()); verify(issueStore, never()).replaceAllIssuesOfFile(eq("branch"), any(), anyList()); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/ServerVersionAndStatusCheckerTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.exception.UnexpectedBodyException; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ServerVersionAndStatusCheckerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private ServerVersionAndStatusChecker underTest; @BeforeEach void setUp() { underTest = new ServerVersionAndStatusChecker(new ServerApi(mockServer.serverApiHelper())); } @Test void failWhenServerNotReady() { mockServer.addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"5.5-SNAPSHOT\",\"status\": \"DOWN\"}"); var throwable = catchThrowable(() -> underTest.checkVersionAndStatus(new SonarLintCancelMonitor())); assertThat(throwable).hasMessage("Server not ready (DOWN)"); } @Test void failWhenIncompatibleVersion() { mockServer.addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"6.7\",\"status\": \"UP\"}"); var throwable = catchThrowable(() -> underTest.checkVersionAndStatus(new SonarLintCancelMonitor())); assertThat(throwable).hasMessage("Your SonarQube Server instance has version 6.7. Version should be greater or equal to 9.9"); } @Test void shouldNotFailWhenIncompatibleVersionSc() { underTest = new ServerVersionAndStatusChecker(new ServerApi(mockServer.serverApiHelper("orgKey"))); mockServer.addStringResponse("/api/system/status", "{\"id\": \"20160308094653\",\"version\": \"6.7\",\"status\": \"UP\"}"); var throwable = catchThrowable(() -> underTest.checkVersionAndStatus(new SonarLintCancelMonitor())); assertThat(throwable).isNull(); } @Test void responseParsingError() { mockServer.addStringResponse("/api/system/status", "bla bla"); var throwable = catchThrowable(() -> underTest.checkVersionAndStatus(new SonarLintCancelMonitor())); assertThat(throwable).isInstanceOf(UnexpectedBodyException.class); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/StorageExceptionTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.IOException; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverconnection.storage.StorageException; import static org.assertj.core.api.Assertions.assertThat; class StorageExceptionTests { @Test void withCauseAndMessage() { var cause = new IOException("cause"); var ex = new StorageException("msg", cause); assertThat(ex.getCause()).isEqualTo(cause); assertThat(ex.getMessage()).isEqualTo("msg"); assertThat(ex.getStackTrace()).isNotEmpty(); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/TaintIssueDownloaderTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.SonarLanguage; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Flow; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Paging; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.Severity; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Common.TextRange; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.Location; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Issues.TaintVulnerabilityLite; import org.sonarsource.sonarlint.core.serverapi.proto.sonarqube.ws.Rules; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoImpactSeverity; import static org.sonarsource.sonarlint.core.serverconnection.DownloaderUtils.parseProtoSoftwareQuality; import static org.sonarsource.sonarlint.core.serverconnection.TaintIssueDownloader.hash; import static org.sonarsource.sonarlint.core.serverconnection.TaintIssueDownloader.parseProtoCleanCodeAttribute; class TaintIssueDownloaderTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String PROJECT_KEY = "project"; private static final String FILE_1_KEY = PROJECT_KEY + ":foo/bar/Hello.java"; private static final String FILE_2_KEY = PROJECT_KEY + ":foo/bar/Hello2.java"; private static final String FILE_3_KEY = PROJECT_KEY + ":foo/bar/Hello3.java"; private static final String DUMMY_KEY = "dummyKey"; @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); private ServerApi serverApi; private TaintIssueDownloader underTest; @BeforeEach void prepare() { underTest = new TaintIssueDownloader(Set.of(SonarLanguage.JAVA)); serverApi = new ServerApi(mockServer.serverApiHelper()); } @Test void test_download_vulnerabilities_from_issue_search() { var ruleSearchResponse = Rules.SearchResponse.newBuilder() .setTotal(1) .addRules(Rules.Rule.newBuilder() .setKey("javasecurity:S789")) .build(); var issueSearchResponse = Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder() .setKey("uuid1") .setRule("javasecurity:S789") .setHash("hash2") .setMessage("Primary message 2") .setTextRange(TextRange.newBuilder().setStartLine(2).setStartOffset(7).setEndLine(4).setEndOffset(9)) .setCreationDate("2021-01-11T18:17:31+0000") .setComponent(FILE_1_KEY) .setType(Common.RuleType.VULNERABILITY) .setSeverity(Severity.INFO) .setCleanCodeAttribute(Common.CleanCodeAttribute.COMPLETE) .addImpacts(Common.Impact.newBuilder() .setSoftwareQuality(Common.SoftwareQuality.SECURITY) .setSeverity(Common.ImpactSeverity.HIGH) .build()) .addFlows(Flow.newBuilder() .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Location 1").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(5).setEndOffset(6))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Invalid text range").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(7).setEndOffset(6))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Another file").setComponent(FILE_2_KEY) .setTextRange(TextRange.newBuilder().setStartLine(9).setStartOffset(10).setEndLine(11).setEndOffset(12))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Location No Text Range").setComponent(FILE_3_KEY))) .addFlows(Flow.newBuilder() .addLocations(Common.Location.newBuilder().setMsg("Flow 2 - Location 1").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(5).setEndOffset(6)))) .setRuleDescriptionContextKey("context1")) .addIssues(Issues.Issue.newBuilder() .setKey("uuid2") .setRule("javasecurity:S789") .setMessage("Project level issue") .setCreationDate("2021-01-11T18:17:31+0000") .setComponent(PROJECT_KEY) .setType(Common.RuleType.VULNERABILITY) .setSeverity(Severity.CRITICAL) .addFlows(Flow.newBuilder() .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Location 1").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(5).setEndOffset(6))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Invalid text range").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(7).setEndOffset(6))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Another file").setComponent(FILE_2_KEY) .setTextRange(TextRange.newBuilder().setStartLine(9).setStartOffset(10).setEndLine(11).setEndOffset(12))) .addLocations(Common.Location.newBuilder().setMsg("Flow 1 - Location No Text Range").setComponent(FILE_3_KEY))) .addFlows(Flow.newBuilder() .addLocations(Common.Location.newBuilder().setMsg("Flow 2 - Location 1").setComponent(FILE_1_KEY) .setTextRange(TextRange.newBuilder().setStartLine(5).setStartOffset(1).setEndLine(5).setEndOffset(6)))) .setRuleDescriptionContextKey("context2")) .addComponents(Issues.Component.newBuilder() .setKey(PROJECT_KEY)) .addComponents(Issues.Component.newBuilder() .setKey(FILE_1_KEY) .setPath("foo/bar/Hello.java")) .addComponents(Issues.Component.newBuilder() .setKey(FILE_2_KEY) .setPath("foo/bar/Hello2.java")) .addComponents(Issues.Component.newBuilder() .setKey(FILE_3_KEY) .setPath("foo/bar/Hello3.java")) .setPaging(Paging.newBuilder() .setPageIndex(1) .setPageSize(500) .setTotal(1)) .build(); mockServer.addProtobufResponse( "/api/rules/search.protobuf?repositories=roslyn.sonaranalyzer.security.cs,javasecurity,jssecurity,kotlinsecurity,phpsecurity,pythonsecurity,tssecurity,vbnetsecurity,gosecurity&f=repo&s=key&ps=500&p=1", ruleSearchResponse); mockServer.addProtobufResponse( "/api/issues/search.protobuf?statuses=OPEN,CONFIRMED,REOPENED,RESOLVED&types=VULNERABILITY&componentKeys=" + DUMMY_KEY + "&components=" + DUMMY_KEY + "&rules=javasecurity%3AS789&ps=500&p=1", issueSearchResponse); mockServer.addStringResponse("/api/sources/raw?key=" + URLEncoder.encode(FILE_1_KEY, StandardCharsets.UTF_8), "Even\nBefore My\n\tCode\n Snippet And\n After"); var issues = underTest.downloadTaintFromIssueSearch(serverApi, DUMMY_KEY, null, new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); var taintIssue = issues.get(0); assertThat(taintIssue.getMessage()).isEqualTo("Primary message 2"); assertThat(taintIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(taintIssue.getType()).isEqualTo(RuleType.VULNERABILITY); assertThat(taintIssue.getSeverity()).isEqualTo(IssueSeverity.INFO); assertThat(taintIssue.getCleanCodeAttribute()).hasValue(CleanCodeAttribute.COMPLETE); assertThat(taintIssue.getImpacts()).containsExactly(entry(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)); assertTextRange(taintIssue.getTextRange(), 2, 7, 4, 9, hash("My\n\tCode\n Snippet")); assertThat(taintIssue.getFlows()).hasSize(2); assertThat(taintIssue.getFlows().get(0).locations()).hasSize(4); var flowLocation11 = taintIssue.getFlows().get(0).locations().get(0); assertThat(flowLocation11.filePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertTextRange(flowLocation11.textRange(), 5, 1, 5, 6, hash("After")); // Invalid text range assertThat(taintIssue.getFlows().get(0).locations().get(1).textRange().getHash()).isEmpty(); // 404 assertThat(taintIssue.getFlows().get(0).locations().get(2).textRange().getHash()).isEmpty(); // No text range assertThat(taintIssue.getFlows().get(0).locations().get(3).textRange()).isNull(); assertThat(taintIssue.getFlows().get(1).locations()).hasSize(1); assertThat(taintIssue.getRuleDescriptionContextKey()).isEqualTo("context1"); } @Test void test_filter_taint_issues_by_branch_if_branch_parameter_provided() { var response = Issues.SearchWsResponse.newBuilder() .addIssues(Issues.Issue.newBuilder() .setRule("javasecurity:S789") .setCreationDate("2021-01-11T18:17:31+0000") .setComponent(FILE_1_KEY) .setType(Common.RuleType.BUG) .setSeverity(Severity.INFO)) .addComponents(Issues.Component.newBuilder() .setKey(FILE_1_KEY) .setPath("foo/bar/Hello2.java")) .setPaging(Paging.newBuilder() .setPageIndex(1) .setPageSize(500) .setTotal(1)) .build(); var ruleSearchResponse = Rules.SearchResponse.newBuilder() .setTotal(1) .addRules(Rules.Rule.newBuilder() .setKey("javasecurity:S789")) .build(); mockServer.addProtobufResponse( "/api/rules/search.protobuf?repositories=roslyn.sonaranalyzer.security.cs,javasecurity,jssecurity,kotlinsecurity,phpsecurity,pythonsecurity,tssecurity,vbnetsecurity,gosecurity&f=repo&s=key&ps=500&p=1", ruleSearchResponse); mockServer.addProtobufResponse( "/api/issues/search.protobuf?statuses=OPEN,CONFIRMED,REOPENED,RESOLVED&types=VULNERABILITY&componentKeys=dummyKey&components=dummyKey&rules=javasecurity%3AS789&branch=branchName&ps=500&p=1", response); var issues = underTest.downloadTaintFromIssueSearch(serverApi, DUMMY_KEY, "branchName", new SonarLintCancelMonitor()); assertThat(issues).hasSize(1); } @Test void test_download_taint_issues_from_pull_ws() { var timestamp = Issues.IssuesPullQueryTimestamp.newBuilder().setQueryTimestamp(123L).build(); var taint1 = TaintVulnerabilityLite.newBuilder() .setKey("uuid1") .setRuleKey("sonarjava:S123") .setType(Common.RuleType.VULNERABILITY) .setSeverity(Severity.MAJOR) .setCleanCodeAttribute(Common.CleanCodeAttribute.COMPLETE) .addImpacts(Common.Impact.newBuilder() .setSoftwareQuality(Common.SoftwareQuality.SECURITY) .setSeverity(Common.ImpactSeverity.HIGH) .build()) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message") .setTextRange(Issues.TextRange.newBuilder().setStartLine(1).setStartLineOffset(2).setEndLine(3) .setEndLineOffset(4).setHash("hash"))) .setCreationDate(123456789L) .addFlows(Issues.Flow.newBuilder() .addLocations(Location.newBuilder().setMessage("Flow 1 - Location 1").setFilePath("foo/bar/Hello.java") .setTextRange(Issues.TextRange.newBuilder().setStartLine(5).setStartLineOffset(1).setEndLine(5).setEndLineOffset(6).setHash("hashLocation11"))) .addLocations(Location.newBuilder().setMessage("Flow 1 - Another file").setFilePath("foo/bar/Hello2.java") .setTextRange(Issues.TextRange.newBuilder().setStartLine(9).setStartLineOffset(10).setEndLine(11).setEndLineOffset(12).setHash("hashLocation12"))) .addLocations(Location.newBuilder().setMessage("Flow 1 - Location No Text Range").setFilePath("foo/bar/Hello.java"))) .addFlows(Issues.Flow.newBuilder() .addLocations(Location.newBuilder().setMessage("Flow 2 - Location 1").setFilePath("foo/bar/Hello.java") .setTextRange(Issues.TextRange.newBuilder().setStartLine(5).setStartLineOffset(1).setEndLine(5).setEndLineOffset(6).setHash("hashLocation21")))) .setRuleDescriptionContextKey("context") .build(); var taintNoRange = TaintVulnerabilityLite.newBuilder() .setKey("uuid2") .setRuleKey("sonarjava:S123") .setType(Common.RuleType.VULNERABILITY) .setSeverity(Common.Severity.MINOR) .setMainLocation(Location.newBuilder().setFilePath("foo/bar/Hello.java").setMessage("Primary message")) .setCreationDate(123456789L) .build(); var taintResolved = TaintVulnerabilityLite.newBuilder(taint1) .setResolved(true) .build(); mockServer.addProtobufResponseDelimited("/api/issues/pull_taint?projectKey=" + DUMMY_KEY + "&branchName=myBranch&languages=java", timestamp, taint1, taintNoRange, taintResolved); var result = underTest.downloadTaintFromPull(serverApi, DUMMY_KEY, "myBranch", Optional.empty(), new SonarLintCancelMonitor()); assertThat(result.getQueryTimestamp()).isEqualTo(Instant.ofEpochMilli(123L)); assertThat(result.getChangedTaintIssues()).hasSize(3); assertThat(result.getClosedIssueKeys()).isEmpty(); var serverTaintIssue = result.getChangedTaintIssues().get(0); assertThat(serverTaintIssue.getSonarServerKey()).isEqualTo("uuid1"); assertThat(serverTaintIssue.getMessage()).isEqualTo("Primary message"); assertThat(serverTaintIssue.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(serverTaintIssue.getSeverity()).isEqualTo(IssueSeverity.MAJOR); assertThat(serverTaintIssue.getType()).isEqualTo(RuleType.VULNERABILITY); assertThat(serverTaintIssue.getCleanCodeAttribute()).hasValue(CleanCodeAttribute.COMPLETE); assertThat(serverTaintIssue.getImpacts()).containsExactly(entry(SoftwareQuality.SECURITY, ImpactSeverity.HIGH)); assertTextRange(serverTaintIssue.getTextRange(), 1, 2, 3, 4, "hash"); assertThat(serverTaintIssue.getFlows()).hasSize(2); assertThat(serverTaintIssue.getFlows().get(0).locations()).hasSize(3); var flowLocation11 = serverTaintIssue.getFlows().get(0).locations().get(0); assertThat(flowLocation11.filePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertTextRange(flowLocation11.textRange(), 5, 1, 5, 6, "hashLocation11"); // No text range assertThat(serverTaintIssue.getFlows().get(0).locations().get(2).textRange()).isNull(); assertThat(serverTaintIssue.getFlows().get(1).locations()).hasSize(1); assertThat(serverTaintIssue.getRuleDescriptionContextKey()).isEqualTo("context"); var taintIssueNoRange = result.getChangedTaintIssues().get(1); assertThat(taintIssueNoRange.getSonarServerKey()).isEqualTo("uuid2"); assertThat(taintIssueNoRange.getFilePath()).isEqualTo(Path.of("foo/bar/Hello.java")); assertThat(taintIssueNoRange.getTextRange()).isNull(); var resolvedTaint = result.getChangedTaintIssues().get(2); assertThat(resolvedTaint.isResolved()).isTrue(); } @Test void parse_clean_code_attribute_from_stream() { var partialIssue = Issues.Issue.newBuilder().setCleanCodeAttribute(Common.CleanCodeAttribute.CLEAR).build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isEqualTo(CleanCodeAttribute.CLEAR); } @Test void parse_clean_code_attribute_from_stream_missing() { var partialIssue = Issues.Issue.newBuilder().build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isNull(); } @Test void parse_clean_code_attribute_from_stream_unknown() { var partialIssue = Issues.Issue.newBuilder().setCleanCodeAttribute(Common.CleanCodeAttribute.UNKNOWN_ATTRIBUTE).build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isNull(); } @Test void parse_clean_code_attribute_from_lite_stream() { var partialIssue = Issues.TaintVulnerabilityLite.newBuilder().setKey("key").setCleanCodeAttribute(Common.CleanCodeAttribute.CLEAR).build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isEqualTo(CleanCodeAttribute.CLEAR); } @Test void parse_clean_code_attribute_from_lite_stream_missing() { var partialIssue = Issues.TaintVulnerabilityLite.newBuilder().setKey("key").build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isNull(); } @Test void parse_clean_code_attribute_from_lite_stream_unknown() { var partialIssue = Issues.TaintVulnerabilityLite.newBuilder().setKey("key").setCleanCodeAttribute(Common.CleanCodeAttribute.UNKNOWN_ATTRIBUTE).build(); var cleanCodeAttribute = parseProtoCleanCodeAttribute(partialIssue); assertThat(cleanCodeAttribute).isNull(); } @Test void parse_software_quality_and_impact_severity() { var impact = Common.Impact.newBuilder().setSoftwareQuality(Common.SoftwareQuality.SECURITY).setSeverity(Common.ImpactSeverity.MEDIUM).build(); assertThat(parseProtoSoftwareQuality(impact)).isEqualTo(SoftwareQuality.SECURITY); assertThat(parseProtoImpactSeverity(impact)).isEqualTo(ImpactSeverity.MEDIUM); } @Test void parse_software_quality_and_impact_severity_info() { var impact = Common.Impact.newBuilder().setSoftwareQuality(Common.SoftwareQuality.SECURITY).setSeverity(Common.ImpactSeverity.ImpactSeverity_INFO).build(); assertThat(parseProtoSoftwareQuality(impact)).isEqualTo(SoftwareQuality.SECURITY); assertThat(parseProtoImpactSeverity(impact)).isEqualTo(ImpactSeverity.INFO); } @Test void parse_software_quality_and_impact_severity_blocker() { var impact = Common.Impact.newBuilder().setSoftwareQuality(Common.SoftwareQuality.SECURITY).setSeverity(Common.ImpactSeverity.ImpactSeverity_BLOCKER).build(); assertThat(parseProtoSoftwareQuality(impact)).isEqualTo(SoftwareQuality.SECURITY); assertThat(parseProtoImpactSeverity(impact)).isEqualTo(ImpactSeverity.BLOCKER); } @Test void parse_software_quality_unknown() { var impact = Common.Impact.newBuilder().setSoftwareQuality(Common.SoftwareQuality.UNKNOWN_IMPACT_QUALITY).setSeverity(Common.ImpactSeverity.HIGH).build(); assertThatThrownBy(() -> parseProtoSoftwareQuality(impact)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unknown or missing software quality"); } @Test void parse_impact_severity_unknown() { var impact = Common.Impact.newBuilder().setSeverity(Common.ImpactSeverity.UNKNOWN_IMPACT_SEVERITY).setSoftwareQuality(Common.SoftwareQuality.MAINTAINABILITY).build(); assertThatThrownBy(() -> parseProtoImpactSeverity(impact)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unknown or missing impact severity"); } private static void assertTextRange(@Nullable TextRangeWithHash textRangeWithHash, int startLine, int startLineOffset, int endLine, int endLineOffset, String hash) { assertThat(textRangeWithHash).isNotNull(); assertThat(textRangeWithHash.getStartLine()).isEqualTo(startLine); assertThat(textRangeWithHash.getStartLineOffset()).isEqualTo(startLineOffset); assertThat(textRangeWithHash.getEndLine()).isEqualTo(endLine); assertThat(textRangeWithHash.getEndLineOffset()).isEqualTo(endLineOffset); assertThat(textRangeWithHash.getHash()).isEqualTo(hash); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/UserSynchronizerTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.serverapi.ServerApi; import testutils.MockWebServerExtensionWithProtobuf; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; class UserSynchronizerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf(); @TempDir Path tmpDir; private UserSynchronizer synchronizer; private ConnectionStorage storage; @BeforeEach void prepare() { var databaseService = mock(SonarLintDatabase.class); storage = new ConnectionStorage(tmpDir, "connectionId", databaseService); synchronizer = new UserSynchronizer(storage); } @Test void it_should_synchronize_user_id_on_sonarcloud() { mockServer.addStringResponse("/api/users/current", """ { "isLoggedIn": true, "id": "16c9b3b3-3f7e-4d61-91fe-31d731456c08", "login": "obiwan.kenobi" }"""); var serverApi = new ServerApi(mockServer.endpointParams("orgKey"), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); synchronizer.synchronize(serverApi, new SonarLintCancelMonitor()); var storedUserId = storage.user().read(); assertThat(storedUserId) .isPresent() .contains("16c9b3b3-3f7e-4d61-91fe-31d731456c08"); } @Test void it_should_synchronize_user_id_on_sonarqube_server() { mockServer.addStringResponse("/api/users/current", """ { "isLoggedIn": true, "id": "00000000-0000-0000-0000-000000000001", "login": "obiwan.kenobi" }"""); var serverApi = new ServerApi(mockServer.endpointParams(), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); synchronizer.synchronize(serverApi, new SonarLintCancelMonitor()); var storedUserId = storage.user().read(); assertThat(storedUserId) .isPresent() .contains("00000000-0000-0000-0000-000000000001"); } @Test void it_should_not_store_null_user_id() { mockServer.addStringResponse("/api/users/current", "{}"); var serverApi = new ServerApi(mockServer.endpointParams("orgKey"), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); synchronizer.synchronize(serverApi, new SonarLintCancelMonitor()); var storedUserId = storage.user().read(); assertThat(storedUserId).isEmpty(); } @Test void it_should_store_user_id_in_correct_file() throws IOException { mockServer.addStringResponse("/api/users/current", """ { "isLoggedIn": true, "id": "test-user-id", "login": "test.user" }"""); var serverApi = new ServerApi(mockServer.endpointParams("orgKey"), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); synchronizer.synchronize(serverApi, new SonarLintCancelMonitor()); var connectionPath = tmpDir.resolve("636f6e6e656374696f6e4964"); var userFile = connectionPath.resolve("user.pb"); assertThat(userFile).exists(); assertThat(Files.size(userFile)).isGreaterThan(0); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/VersionUtilsTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.Version; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.serverconnection.VersionUtils.getCurrentLts; import static org.sonarsource.sonarlint.core.serverconnection.VersionUtils.getMinimalSupportedVersion; class VersionUtilsTests { @Test void grace_period_should_be_false_if_connected_current_lts() { assertThat(VersionUtils.isVersionSupportedDuringGracePeriod(getCurrentLts())).isFalse(); assertThat(VersionUtils.isVersionSupportedDuringGracePeriod(Version.create(getCurrentLts().getName() + ".1"))).isFalse(); } @Test void grace_period_should_be_false_if_connected_outdated_version() { assertThat(VersionUtils.isVersionSupportedDuringGracePeriod(Version.create("5.9"))).isFalse(); } @Test void grace_period_should_be_true_if_connected_during_grace_period() { // read isVersionSupportedDuringGracePeriod javadoc assertThat(VersionUtils.isVersionSupportedDuringGracePeriod(getMinimalSupportedVersion())).isFalse(); assertThat(VersionUtils.isVersionSupportedDuringGracePeriod(Version.create(getMinimalSupportedVersion().getName() + ".1"))).isFalse(); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/aicodefix/AiCodeFixRepositoryTest.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.aicodefix; import java.nio.file.Path; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import static org.assertj.core.api.Assertions.assertThat; class AiCodeFixRepositoryTest { @RegisterExtension static SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path temp; @Test void upsert_and_get_should_persist_to_h2_file_database() { // Given a file-based H2 database under a temporary storage root var storageRoot = temp.resolve("storage"); var db = new SonarLintDatabase(storageRoot); var aiCodeFixRepo = new AiCodeFixRepository(db.dsl()); var entityToStore = new AiCodeFix( "test-connection", Set.of("java:S100", "js:S200"), true, AiCodeFix.Enablement.ENABLED_FOR_SOME_PROJECTS, Set.of("project-a", "project-b") ); // When we upsert the entity aiCodeFixRepo.upsert(entityToStore); // And shutdown the first DB to force closing connections db.shutdown(); // Create a new repository with a fresh DB instance pointing to the same storage root var db2 = new SonarLintDatabase(storageRoot); var repo2 = new AiCodeFixRepository(db2.dsl()); // With a different connection id, no settings should be visible var loadedOptDifferent = repo2.get("test-connection-2"); assertThat(loadedOptDifferent).isEmpty(); // With the same connection id, we should read back exactly what we stored var repoSame = new AiCodeFixRepository(db2.dsl()); var loadedOpt = repoSame.get("test-connection"); assertThat(loadedOpt).isPresent(); var loaded = loadedOpt.get(); assertThat(loaded.supportedRules()).containsExactlyInAnyOrder("java:S100", "js:S200"); assertThat(loaded.organizationEligible()).isTrue(); assertThat(loaded.enablement()).isEqualTo(AiCodeFix.Enablement.ENABLED_FOR_SOME_PROJECTS); assertThat(loaded.enabledProjectKeys()).containsExactlyInAnyOrder("project-a", "project-b"); db2.shutdown(); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/issues/KnownFindingsRepositoryTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.KnownFinding; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; class KnownFindingsRepositoryTests { @RegisterExtension static SonarLintLogTester logTester = new SonarLintLogTester(); private SonarLintDatabase db; @AfterEach void shutdown() { db.shutdown(); } @Test void testKnownFindingsRepository(@TempDir Path temp) { var storageRoot = temp.resolve("storage"); db = new SonarLintDatabase(storageRoot); var repo = new KnownFindingsRepository(db); var filePath = Path.of("/file/path"); var configScopeId = "configScopeId"; var issues = new ArrayList(); var issueUuid1 = UUID.randomUUID(); var issueIntroDate1 = Instant.now(); var issue1 = new KnownFinding(issueUuid1, "test-message", new TextRangeWithHash(1, 2, 3, 4, "hash1"), new LineWithHash(1, "hash"), "test-issue-rule-1", "Test issue message 1", issueIntroDate1); issues.add(issue1); var issueUuid2 = UUID.randomUUID(); var issueIntroDate2 = Instant.now(); var issue2 = new KnownFinding(issueUuid2, "test-message", new TextRangeWithHash(5, 6, 7, 8, "hash2"), new LineWithHash(1, "hash"), "test-issue-rule-2", "Test issue message 2", issueIntroDate2); issues.add(issue2); var hotspots = new ArrayList(); var hotspotUuid1 = UUID.randomUUID(); var hotspotIntroDate1 = Instant.now(); var hotspot1 = new KnownFinding(hotspotUuid1, "test-message", new TextRangeWithHash(1, 2, 3, 4, "hash1"), new LineWithHash(1, "hash"), "test-hotspot-rule-1", "Test hotspot message 1", hotspotIntroDate1); hotspots.add(hotspot1); var hotspotUuid2 = UUID.randomUUID(); var hotspotIntroDate2 = Instant.now(); var hotspot2 = new KnownFinding(hotspotUuid2, "test-message", new TextRangeWithHash(5, 6, 7, 8, "hash2"), new LineWithHash(1, "hash"), "test-hotspot-rule-2", "Test hotspot message 2", hotspotIntroDate2); hotspots.add(hotspot2); repo.storeKnownIssues(configScopeId, filePath, issues); repo.storeKnownSecurityHotspots(configScopeId, filePath, hotspots); var knownIssues = repo.loadIssuesForFile(configScopeId, filePath); var knownHotspots = repo.loadSecurityHotspotsForFile(configScopeId, filePath); assertThat(knownIssues).hasSize(2); assertThat(knownHotspots).hasSize(2); var knownIssue = knownIssues.get(0); assertThat(knownIssue.getRuleKey()).isEqualTo(issue1.getRuleKey()); assertThat(knownIssue.getServerKey()).isEqualTo(issue1.getServerKey()); assertThat(knownIssue.getMessage()).isEqualTo(issue1.getMessage()); assertThat(knownIssue.getTextRangeWithHash()).isEqualTo(issue1.getTextRangeWithHash()); assertThat(knownIssue.getLineWithHash().getNumber()).isEqualTo(issue1.getLineWithHash().getNumber()); assertThat(knownIssue.getLineWithHash().getHash()).isEqualTo(issue1.getLineWithHash().getHash()); var knownHotspot = knownHotspots.get(0); assertThat(knownHotspot.getRuleKey()).isEqualTo(hotspot1.getRuleKey()); assertThat(knownHotspot.getServerKey()).isEqualTo(hotspot1.getServerKey()); assertThat(knownHotspot.getMessage()).isEqualTo(hotspot1.getMessage()); assertThat(knownHotspot.getTextRangeWithHash()).isEqualTo(hotspot1.getTextRangeWithHash()); assertThat(knownHotspot.getLineWithHash().getNumber()).isEqualTo(hotspot1.getLineWithHash().getNumber()); assertThat(knownHotspot.getLineWithHash().getHash()).isEqualTo(hotspot1.getLineWithHash().getHash()); } @Test void should_allow_for_a_long_message(@TempDir Path temp) { var storageRoot = temp.resolve("storage"); db = new SonarLintDatabase(storageRoot); var repo = new KnownFindingsRepository(db); var longMessage = "m".repeat(10000); var path = Path.of("path"); repo.storeKnownIssues("configScope", path, List.of(new KnownFinding(UUID.randomUUID(), "serverKey", null, null, "rule:key", longMessage, Instant.now()))); var issues = repo.loadIssuesForFile("configScope", path); assertThat(issues).hasSize(1); assertThat(issues.get(0).getMessage()).isEqualTo(longMessage); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/issues/LocalOnlyIssuesRepositoryTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.LineWithHash; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssue; import org.sonarsource.sonarlint.core.commons.LocalOnlyIssueResolution; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import static org.assertj.core.api.Assertions.assertThat; class LocalOnlyIssuesRepositoryTests { @RegisterExtension static SonarLintLogTester logTester = new SonarLintLogTester(); private LocalOnlyIssuesRepository repository; private SonarLintDatabase db; @BeforeEach void prepare(@TempDir Path temp) { var storageRoot = temp.resolve("storage"); db = new SonarLintDatabase(storageRoot); repository = new LocalOnlyIssuesRepository(db.dsl()); } @AfterEach void shutdown() { db.shutdown(); } @Test void should_store_and_load_issues_for_file() { var filePath = Path.of("/file/path"); var configScopeId = "configScopeId"; var issueUuid1 = UUID.randomUUID(); var issue1 = new LocalOnlyIssue(issueUuid1, filePath, new TextRangeWithHash(1, 2, 3, 4, "hash1"), new LineWithHash(1, "linehash1"), "test-rule-1", "Test issue message 1", null); var issueUuid2 = UUID.randomUUID(); var issue2 = new LocalOnlyIssue(issueUuid2, filePath, new TextRangeWithHash(5, 6, 7, 8, "hash2"), new LineWithHash(2, "linehash2"), "test-rule-2", "Test issue message 2", null); repository.storeLocalOnlyIssue(configScopeId, issue1); repository.storeLocalOnlyIssue(configScopeId, issue2); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(2); assertThat(loadedIssues).extracting(LocalOnlyIssue::getId).containsExactlyInAnyOrder(issueUuid1, issueUuid2); var loadedIssue1 = loadedIssues.stream().filter(i -> i.getId().equals(issueUuid1)).findFirst().orElseThrow(); assertThat(loadedIssue1.getRuleKey()).isEqualTo("test-rule-1"); assertThat(loadedIssue1.getMessage()).isEqualTo("Test issue message 1"); assertThat(loadedIssue1.getTextRangeWithHash()).isEqualTo(new TextRangeWithHash(1, 2, 3, 4, "hash1")); assertThat(loadedIssue1.getLineWithHash().getNumber()).isEqualTo(1); assertThat(loadedIssue1.getLineWithHash().getHash()).isEqualTo("linehash1"); assertThat(loadedIssue1.getResolution()).isNull(); } @Test void should_store_and_load_resolved_issue() { var filePath = Path.of("/file/path"); var configScopeId = "configScopeId"; var issueUuid = UUID.randomUUID(); var resolutionDate = Instant.now().truncatedTo(ChronoUnit.MILLIS); var resolution = new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, resolutionDate, "Test comment"); var issue = new LocalOnlyIssue(issueUuid, filePath, new TextRangeWithHash(1, 2, 3, 4, "hash1"), new LineWithHash(1, "linehash1"), "test-rule-1", "Test issue message", resolution); repository.storeLocalOnlyIssue(configScopeId, issue); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(1); var loadedIssue = loadedIssues.get(0); assertThat(loadedIssue.getId()).isEqualTo(issueUuid); assertThat(loadedIssue.getResolution()).isNotNull(); assertThat(loadedIssue.getResolution().getStatus()).isEqualTo(IssueStatus.WONT_FIX); assertThat(loadedIssue.getResolution().getResolutionDate()).isEqualTo(resolutionDate); assertThat(loadedIssue.getResolution().getComment()).isEqualTo("Test comment"); } @Test void should_store_issue_without_text_range_and_line_hash() { var filePath = Path.of("/file/path"); var configScopeId = "configScopeId"; var issueUuid = UUID.randomUUID(); var issue = new LocalOnlyIssue(issueUuid, filePath, null, null, "test-rule-1", "Test issue message", null); repository.storeLocalOnlyIssue(configScopeId, issue); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(1); var loadedIssue = loadedIssues.get(0); assertThat(loadedIssue.getId()).isEqualTo(issueUuid); assertThat(loadedIssue.getTextRangeWithHash()).isNull(); assertThat(loadedIssue.getLineWithHash()).isNull(); } @Test void should_load_all_issues_for_configuration_scope() { var configScopeId = "configScopeId"; var filePath1 = Path.of("/file/path1"); var filePath2 = Path.of("/file/path2"); var issue1 = new LocalOnlyIssue(UUID.randomUUID(), filePath1, null, null, "test-rule-1", "Message 1", null); var issue2 = new LocalOnlyIssue(UUID.randomUUID(), filePath2, null, null, "test-rule-2", "Message 2", null); var issue3 = new LocalOnlyIssue(UUID.randomUUID(), filePath1, null, null, "test-rule-3", "Message 3", null); repository.storeLocalOnlyIssue(configScopeId, issue1); repository.storeLocalOnlyIssue(configScopeId, issue2); repository.storeLocalOnlyIssue(configScopeId, issue3); var allIssues = repository.loadAll(configScopeId); assertThat(allIssues).hasSize(3); assertThat(allIssues).extracting(LocalOnlyIssue::getId) .containsExactlyInAnyOrder(issue1.getId(), issue2.getId(), issue3.getId()); } @Test void should_find_issue_by_id() { var configScopeId = "configScopeId"; var issueUuid = UUID.randomUUID(); var issue = new LocalOnlyIssue(issueUuid, Path.of("/file/path"), null, null, "test-rule-1", "Test message", null); repository.storeLocalOnlyIssue(configScopeId, issue); var foundIssue = repository.find(issueUuid); assertThat(foundIssue).isPresent(); assertThat(foundIssue.get().getId()).isEqualTo(issueUuid); assertThat(foundIssue.get().getRuleKey()).isEqualTo("test-rule-1"); assertThat(foundIssue.get().getMessage()).isEqualTo("Test message"); } @Test void should_return_empty_when_issue_not_found() { var foundIssue = repository.find(UUID.randomUUID()); assertThat(foundIssue).isEmpty(); } @Test void should_remove_issue() { var configScopeId = "configScopeId"; var filePath = Path.of("/file/path"); var issueUuid1 = UUID.randomUUID(); var issueUuid2 = UUID.randomUUID(); var issue1 = new LocalOnlyIssue(issueUuid1, filePath, null, null, "test-rule-1", "Message 1", null); var issue2 = new LocalOnlyIssue(issueUuid2, filePath, null, null, "test-rule-2", "Message 2", null); repository.storeLocalOnlyIssue(configScopeId, issue1); repository.storeLocalOnlyIssue(configScopeId, issue2); var removed = repository.removeIssue(issueUuid1); assertThat(removed).isTrue(); var remainingIssues = repository.loadForFile(configScopeId, filePath); assertThat(remainingIssues).hasSize(1); assertThat(remainingIssues.get(0).getId()).isEqualTo(issueUuid2); } @Test void should_return_false_when_removing_nonexistent_issue() { var removed = repository.removeIssue(UUID.randomUUID()); assertThat(removed).isFalse(); } @Test void should_remove_all_issues_for_file() { var configScopeId = "configScopeId"; var filePath1 = Path.of("/file/path1"); var filePath2 = Path.of("/file/path2"); var issue1 = new LocalOnlyIssue(UUID.randomUUID(), filePath1, null, null, "test-rule-1", "Message 1", null); var issue2 = new LocalOnlyIssue(UUID.randomUUID(), filePath1, null, null, "test-rule-2", "Message 2", null); var issue3 = new LocalOnlyIssue(UUID.randomUUID(), filePath2, null, null, "test-rule-3", "Message 3", null); repository.storeLocalOnlyIssue(configScopeId, issue1); repository.storeLocalOnlyIssue(configScopeId, issue2); repository.storeLocalOnlyIssue(configScopeId, issue3); var removed = repository.removeAllIssuesForFile(configScopeId, filePath1); assertThat(removed).isTrue(); assertThat(repository.loadForFile(configScopeId, filePath1)).isEmpty(); assertThat(repository.loadForFile(configScopeId, filePath2)).hasSize(1); } @Test void should_update_existing_issue() { var configScopeId = "configScopeId"; var filePath = Path.of("/file/path"); var issueUuid = UUID.randomUUID(); var issue1 = new LocalOnlyIssue(issueUuid, filePath, null, null, "test-rule-1", "Original message", null); repository.storeLocalOnlyIssue(configScopeId, issue1); var resolution = new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, Instant.now().truncatedTo(ChronoUnit.MILLIS), "Updated comment"); var issue2 = new LocalOnlyIssue(issueUuid, filePath, new TextRangeWithHash(1, 2, 3, 4, "hash"), new LineWithHash(1, "linehash"), "test-rule-1", "Updated message", resolution); repository.storeLocalOnlyIssue(configScopeId, issue2); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(1); var loadedIssue = loadedIssues.get(0); assertThat(loadedIssue.getId()).isEqualTo(issueUuid); assertThat(loadedIssue.getMessage()).isEqualTo("Updated message"); assertThat(loadedIssue.getTextRangeWithHash()).isEqualTo(new TextRangeWithHash(1, 2, 3, 4, "hash")); assertThat(loadedIssue.getLineWithHash().getNumber()).isEqualTo(1); assertThat(loadedIssue.getLineWithHash().getHash()).isEqualTo("linehash"); assertThat(loadedIssue.getResolution()).isNotNull(); assertThat(loadedIssue.getResolution().getStatus()).isEqualTo(IssueStatus.WONT_FIX); assertThat(loadedIssue.getResolution().getComment()).isEqualTo("Updated comment"); } @Test void should_purge_old_resolved_issues() { var configScopeId = "configScopeId"; var filePath = Path.of("/file/path"); var oldDate = Instant.now().minus(10, ChronoUnit.DAYS); var recentDate = Instant.now().minus(1, ChronoUnit.DAYS); var limit = Instant.now().minus(5, ChronoUnit.DAYS); var oldIssue = new LocalOnlyIssue(UUID.randomUUID(), filePath, null, null, "test-rule-1", "Old issue", new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, oldDate.truncatedTo(ChronoUnit.MILLIS), "comment")); var recentIssue = new LocalOnlyIssue(UUID.randomUUID(), filePath, null, null, "test-rule-2", "Recent issue", new LocalOnlyIssueResolution(IssueStatus.WONT_FIX, recentDate.truncatedTo(ChronoUnit.MILLIS), "comment")); var unresolvedIssue = new LocalOnlyIssue(UUID.randomUUID(), filePath, null, null, "test-rule-3", "Unresolved issue", null); repository.storeLocalOnlyIssue(configScopeId, oldIssue); repository.storeLocalOnlyIssue(configScopeId, recentIssue); repository.storeLocalOnlyIssue(configScopeId, unresolvedIssue); repository.purgeIssuesOlderThan(limit); var remainingIssues = repository.loadAll(configScopeId); assertThat(remainingIssues).hasSize(2); assertThat(remainingIssues).extracting(LocalOnlyIssue::getId) .containsExactlyInAnyOrder(recentIssue.getId(), unresolvedIssue.getId()); } @Test void should_isolate_issues_by_configuration_scope() { var configScopeId1 = "configScopeId1"; var configScopeId2 = "configScopeId2"; var filePath = Path.of("/file/path"); var issue1 = new LocalOnlyIssue(UUID.randomUUID(), filePath, null, null, "test-rule-1", "Message 1", null); var issue2 = new LocalOnlyIssue(UUID.randomUUID(), filePath, null, null, "test-rule-2", "Message 2", null); repository.storeLocalOnlyIssue(configScopeId1, issue1); repository.storeLocalOnlyIssue(configScopeId2, issue2); assertThat(repository.loadAll(configScopeId1)).hasSize(1); assertThat(repository.loadAll(configScopeId2)).hasSize(1); assertThat(repository.loadForFile(configScopeId1, filePath)).hasSize(1); assertThat(repository.loadForFile(configScopeId2, filePath)).hasSize(1); } @Test void should_handle_issue_with_only_text_range() { var configScopeId = "configScopeId"; var filePath = Path.of("/file/path"); var issueUuid = UUID.randomUUID(); var issue = new LocalOnlyIssue(issueUuid, filePath, new TextRangeWithHash(1, 2, 3, 4, "hash1"), null, "test-rule-1", "Test message", null); repository.storeLocalOnlyIssue(configScopeId, issue); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(1); var loadedIssue = loadedIssues.get(0); assertThat(loadedIssue.getTextRangeWithHash()).isEqualTo(new TextRangeWithHash(1, 2, 3, 4, "hash1")); assertThat(loadedIssue.getLineWithHash()).isNull(); } @Test void should_handle_issue_with_only_line_hash() { var configScopeId = "configScopeId"; var filePath = Path.of("/file/path"); var issueUuid = UUID.randomUUID(); var issue = new LocalOnlyIssue(issueUuid, filePath, null, new LineWithHash(5, "linehash"), "test-rule-1", "Test message", null); repository.storeLocalOnlyIssue(configScopeId, issue); var loadedIssues = repository.loadForFile(configScopeId, filePath); assertThat(loadedIssues).hasSize(1); var loadedIssue = loadedIssues.get(0); assertThat(loadedIssue.getTextRangeWithHash()).isNull(); assertThat(loadedIssue.getLineWithHash().getNumber()).isEqualTo(5); assertThat(loadedIssue.getLineWithHash().getHash()).isEqualTo("linehash"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/issues/ServerIssueTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.issues; import java.nio.file.Path; import java.time.Instant; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.RuleType; import static org.assertj.core.api.Assertions.assertThat; import static org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueFixtures.aServerIssue; class ServerIssueTests { @Test void testRoundTrips() { var issue = aServerIssue(); var i1 = Instant.ofEpochMilli(100_000_000); assertThat(issue.setCreationDate(i1).getCreationDate()).isEqualTo(i1); assertThat(issue.setFilePath(Path.of("path1")).getFilePath()).isEqualTo(Path.of("path1")); assertThat(issue.setKey("key1").getKey()).isEqualTo("key1"); assertThat(issue.setUserSeverity(IssueSeverity.MAJOR).getUserSeverity()).isEqualTo(IssueSeverity.MAJOR); assertThat(issue.setRuleKey("rule1").getRuleKey()).isEqualTo("rule1"); assertThat(issue.isResolved()).isTrue(); assertThat(issue.setMessage("msg1").getMessage()).isEqualTo("msg1"); assertThat(issue.setType(RuleType.BUG).getType()).isEqualTo(RuleType.BUG); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/prefix/FileTreeMatcherTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.prefix; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class FileTreeMatcherTests { private final FileTreeMatcher fileMatcher = new FileTreeMatcher(); @Test void simple_case_without_prefixes() { List paths = Collections.singletonList(Paths.get("project1/src/main/java/File.java")); var match = fileMatcher.match(paths, paths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("")); } @Test void simple_case_with_prefixes() { List idePaths = Collections.singletonList(Paths.get("local/src/main/java/File.java")); List sqPaths = Collections.singletonList(Paths.get("sq/src/main/java/File.java")); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("local")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("sq")); } @Test void no_match() { List idePaths = Collections.singletonList(Paths.get("local/src/main/java/File1.java")); List sqPaths = Collections.singletonList(Paths.get("sq/src/main/java/File2.java")); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("")); } @Test void empty_project_in_ide() { List idePaths = Collections.emptyList(); List sqPaths = Collections.singletonList(Paths.get("sq/src/main/java/File2.java")); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("")); } @Test void should_return_shortest_sq_prefix_if_there_are_ties() { List idePaths = List.of( Paths.get("pom.xml")); List sqPaths = Arrays.asList( Paths.get("aq1/module2/pom.xml"), Paths.get("aq2/pom.xml"), Paths.get("pom.xml"), Paths.get("aq1/module1/pom.xml")); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("")); sqPaths = Arrays.asList( Paths.get("aq1/module2/pom.xml"), Paths.get("aq2/pom.xml"), Paths.get("aq1/module1/pom.xml")); match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("aq2")); // In case there is also a tie on the prefix segment count, fallback on lexicographic order sqPaths = Arrays.asList( Paths.get("aq1/module2/pom.xml"), Paths.get("aq1/module1/pom.xml")); match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("aq1/module1")); } @Test void more_complex_test_with_multiple_files() throws Exception { List idePaths = Arrays.asList( Paths.get("local/sub/index.html"), Paths.get("local/sub/product1/index.html"), Paths.get("local/sub/product2/index.html"), Paths.get("local/sub/product3/index.html")); List sqPaths = Arrays.asList( Paths.get("sq/index.html"), Paths.get("sq/news/index.html"), Paths.get("sq/news/product1/index.html"), Paths.get("sq/news/product2/index.html"), Paths.get("sq/news/product3/index.html"), Paths.get("sq/products/index.html"), Paths.get("sq/products/product1/index.html"), Paths.get("sq/products/product2/index.html"), Paths.get("sq/products/product3/index.html"), Paths.get("sq/company/index.html"), Paths.get("sq/company/jobs/index.html"), Paths.get("sq/company/news/index.html"), Paths.get("sq/company/contact/index.html")); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("local/sub")); // sq/news is preferred to sq/products because of lexicographic order assertThat(match.sqPrefix()).isEqualTo(Paths.get("sq/news")); } @Disabled("Only used to investigate performance issues like SLCORE-266") @Test void performance_test_worst_case() throws Exception { var depthFactor = 10; var sqNbPerFolder = 10; var sqDepth = 5; var ideNbPerFolder = 10; var ideDepth = 3; var idePaths = generateChildren(Paths.get("local/sub/src/main/java/com/mycompany/myapp/foo/bar"), ideNbPerFolder, depthFactor, ideDepth * depthFactor); System.out.println("IDE file count: " + idePaths.size()); assertThat(idePaths).hasSize((int) Math.pow(ideNbPerFolder, ideDepth + 1)); var sqPaths = generateChildren(Paths.get("sq/src/main/java/com/mycompany/myapp/foo/bar"), sqNbPerFolder, depthFactor, sqDepth * depthFactor); System.out.println("SQ file count: " + sqPaths.size()); assertThat(sqPaths).hasSize((int) Math.pow(sqNbPerFolder, sqDepth + 1)); var start = Instant.now(); var match = fileMatcher.match(sqPaths, idePaths); System.out.println(Duration.between(start, Instant.now()).toMillis() + "ms ellapsed"); assertThat(match.idePrefix()).isEqualTo(Paths.get("local/sub/src/main/java/com/mycompany/myapp/foo/bar")); // sq/folder0/[...]/folder0 is preferred to other sq/folderx because of lexicographic order assertThat(match.sqPrefix()).isEqualTo(Paths.get( "sq/src/main/java/com/mycompany/myapp/foo/bar/folder0/extra49/extra48/extra47/extra46/extra45/extra44/extra43/extra42/extra41/folder0/extra39/extra38/extra37/extra36/extra35/extra34/extra33/extra32/extra31")); } @Disabled("Only used to investigate performance issues like SLCORE-266") @Test void performance_test_only_index_files_with_same_filename() throws Exception { var depthFactor = 10; var sqNbPerFolder = 10; var sqDepth = 5; // IDE contains only paths with filename 'file1.txt' var ideNbPerFolder = 1; var ideDepth = 3; performance_test(depthFactor, sqNbPerFolder, sqDepth, ideNbPerFolder, ideDepth); } private void performance_test(int depthFactor, int sqNbPerFolder, int sqDepth, int ideNbPerFolder, int ideDepth) { var idePaths = generateChildren(Paths.get("local/sub/src/main/java/com/mycompany/myapp/foo/bar"), ideNbPerFolder, depthFactor, ideDepth * depthFactor); System.out.println("IDE file count: " + idePaths.size()); assertThat(idePaths).hasSize((int) Math.pow(ideNbPerFolder, ideDepth + 1)); var sqPaths = generateChildren(Paths.get("sq/src/main/java/com/mycompany/myapp/foo/bar"), sqNbPerFolder, depthFactor, sqDepth * depthFactor); System.out.println("SQ file count: " + sqPaths.size()); assertThat(sqPaths).hasSize((int) Math.pow(sqNbPerFolder, sqDepth + 1)); var start = Instant.now(); var match = fileMatcher.match(sqPaths, idePaths); System.out.println(Duration.between(start, Instant.now()).toMillis() + "ms ellapsed"); assertThat(match.idePrefix()).isEqualTo(Paths.get("local/sub/src/main/java/com/mycompany/myapp/foo/bar")); // sq/folder0/[...]/folder0 is preferred to other sq/folderx because of lexicographic order assertThat(match.sqPrefix()).isEqualTo(Paths.get( "sq/src/main/java/com/mycompany/myapp/foo/bar/folder0/extra49/extra48/extra47/extra46/extra45/extra44/extra43/extra42/extra41/folder0/extra39/extra38/extra37/extra36/extra35/extra34/extra33/extra32/extra31")); } private List generateChildren(Path parent, int count, int everyDepth, int depth) { List result = new ArrayList<>(); if (depth == 0) { for (var i = 0; i < count; i++) { result.add(parent.resolve("file" + i + ".txt")); } } else if (depth % everyDepth == 0) { for (var i = 0; i < count; i++) { var current = parent.resolve("folder" + i); result.addAll(generateChildren(current, count, everyDepth, depth - 1)); } } else { var current = parent.resolve("extra" + depth); result.addAll(generateChildren(current, count, everyDepth, depth - 1)); } return result; } @Test void should_return_most_common_prefixes() { List idePaths = Arrays.asList( Paths.get("local1/src/main/java/A.java"), Paths.get("local1/src/main/java/B.java"), Paths.get("local2/src/main/java/B.java")); List sqPaths = Arrays.asList( Paths.get("sq1/src/main/java/A.java"), Paths.get("sq2/src/main/java/A.java"), Paths.get("sq1/src/main/java/B.java") ); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("local1")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("sq1")); } @Test void should_favor_deepest_common_path() { List idePaths = Arrays.asList( Paths.get("local1/pom.xml"), Paths.get("local1/build.properties"), Paths.get("local1/src/main/java/com/foo/A.java")); List sqPaths = Arrays.asList( Paths.get("sq1/pom.xml"), Paths.get("sq1/build.properties"), Paths.get("sq2/src/main/java/com/foo/A.java") ); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("local1")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("sq2")); } @Test void should_disfavor_path_having_multiple_matches() { List idePaths = Arrays.asList( Paths.get("local1/pom.xml"), Paths.get("local1/build.properties"), Paths.get("local1/src/A.java")); List sqPaths = Arrays.asList( Paths.get("sq1/pom.xml"), Paths.get("sq1/build.properties"), Paths.get("sq2/pom.xml"), Paths.get("sq2/build.properties"), Paths.get("sq3/pom.xml"), Paths.get("sq3/build.properties"), Paths.get("sq4/src/A.java") ); var match = fileMatcher.match(sqPaths, idePaths); assertThat(match.idePrefix()).isEqualTo(Paths.get("local1")); assertThat(match.sqPrefix()).isEqualTo(Paths.get("sq4")); } @Test void verify_equals_and_hashcode_of_result() { var r1 = new FileTreeMatcher.Result(Paths.get("ide1"), Paths.get("sq1")); var r2 = new FileTreeMatcher.Result(Paths.get("ide2"), Paths.get("sq1")); var r3 = new FileTreeMatcher.Result(Paths.get("ide1"), Paths.get("sq2")); var r4 = new FileTreeMatcher.Result(Paths.get("ide1"), Paths.get("sq1")); assertThat(r1.equals(r1)).isTrue(); assertThat(r1.equals(r4)).isTrue(); assertThat(r1).hasSameHashCodeAs(r4); assertThat(r1.equals(r3)).isFalse(); assertThat(r3.equals(r2)).isFalse(); assertThat(r1.equals(new Object())).isFalse(); assertThat(r1.equals(null)).isFalse(); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/prefix/ReversePathTreeTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.prefix; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ReversePathTreeTests { private final ReversePathTree tree = new ReversePathTree(); @Test void should_return_matching_prefix() { tree.index(Paths.get("A/src/main/java/File.java")); var match = tree.findLongestSuffixMatches(Paths.get("B/src/main/java/File.java")); assertThat(match.matchLen()).isEqualTo(4); assertThat(match.matchPrefixes()).containsExactly(Paths.get("A")); } @Test void should_return_matching_prefixes() { tree.index(Paths.get("project1/src/main/java/File.java")); tree.index(Paths.get("project2/src/main/java/File.java")); tree.index(Paths.get("project2/src/test/java/File.java")); var match = tree.findLongestSuffixMatches(Paths.get("src/main/java/File.java")); assertThat(match.matchLen()).isEqualTo(4); assertThat(match.matchPrefixes()).containsExactlyInAnyOrder(Paths.get("project1"), Paths.get("project2")); } @Test void should_return_empty_prefix_if_full_match() { tree.index(Paths.get("project1/src/main/java/File.java")); tree.index(Paths.get("project2/src/main/java/File.java")); tree.index(Paths.get("project2/src/test/java/File.java")); var match = tree.findLongestSuffixMatches(Paths.get("project2/src/main/java/File.java")); assertThat(match.matchLen()).isEqualTo(5); assertThat(match.matchPrefixes()).containsExactly(Paths.get("")); } @Test void should_return_empty_if_no_match() { tree.index(Paths.get("project1/src/main/java/File.java")); tree.index(Paths.get("project2/src/main/java/File.java")); tree.index(Paths.get("project2/src/test/java/File.java")); var match = tree.findLongestSuffixMatches(Paths.get("File2.java")); assertThat(match.matchLen()).isEqualTo(0); assertThat(match.matchPrefixes()).isEmpty(); } @Test void should_return_matches_that_are_part_of_other_matches() { tree.index(Paths.get("project1/A/pom.xml")); tree.index(Paths.get("project1/pom.xml")); tree.index(Paths.get("pom.xml")); var match = tree.findLongestSuffixMatches(Paths.get("pom.xml")); assertThat(match.matchLen()).isEqualTo(1); assertThat(match.matchPrefixes()).containsOnly(Paths.get(""), Paths.get("project1"), Paths.get("project1/A")); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/EntityMapperTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import org.jooq.JSON; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import static org.assertj.core.api.Assertions.assertThat; class EntityMapperTests { private final EntityMapper underTest = new EntityMapper(); @Test void should_serialize_issue_impacts() { var impacts = new EnumMap(SoftwareQuality.class); impacts.put(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH); impacts.put(SoftwareQuality.SECURITY, ImpactSeverity.LOW); var json = underTest.serializeImpacts(impacts); assertThat(json.data()).isEqualTo("{\"MAINTAINABILITY\":\"HIGH\",\"SECURITY\":\"LOW\"}"); var impactsDeserialized = underTest.deserializeImpacts(json); assertThat(impactsDeserialized).isEqualTo(impacts); } @Test void should_serialize_issue_flows() { var flows = new ArrayList(); var path = Path.of("file/path"); var stringPath = path.toString().replace("\\", "\\\\"); flows.add(new ServerTaintIssue.Flow(List.of( new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(1, 2, 3, 4, "hash1"), "Message 1"), new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(5, 6, 7, 8, "hash2"), "Message 2")))); flows.add(new ServerTaintIssue.Flow(List.of( new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(1, 2, 3, 4, "hash1"), "Message 1")))); var taint = new ServerTaintIssue(null, null, true, null, null, null, null, null, null, null, null, null, null, null, flows); var json = underTest.serializeFlows(taint.getFlows()); assertThat(json.data()) .isEqualTo("[{\"locations\":[{\"filePath\":\"" + stringPath + "\"," + "\"textRange\":{\"startLine\":1,\"startLineOffset\":2,\"endLine\":3,\"endLineOffset\":4,\"hash\":\"hash1\"},\"message\":\"Message 1\"}," + "{\"filePath\":\"" + stringPath + "\",\"textRange\":{\"startLine\":5,\"startLineOffset\":6,\"endLine\":7,\"endLineOffset\":8,\"hash\":\"hash2\"}," + "\"message\":\"Message 2\"}]},{\"locations\":[{\"filePath\":\"" + stringPath + "\"," + "\"textRange\":{\"startLine\":1,\"startLineOffset\":2,\"endLine\":3,\"endLineOffset\":4,\"hash\":\"hash1\"},\"message\":\"Message 1\"}]}]"); } @Test void should_deserialize_taint_flows() { var path = Path.of("file/path"); var stringPath = path.toString().replace("\\", "\\\\"); var flows = underTest.deserializeTaintFlows(JSON.valueOf("[{\"locations\":[{\"filePath\":\"" + stringPath + "\"," + "\"textRange\":{\"startLine\":1,\"startLineOffset\":2,\"endLine\":3,\"endLineOffset\":4,\"hash\":\"hash1\"},\"message\":\"Message 1\"}," + "{\"filePath\":\"" + stringPath + "\",\"textRange\":{\"startLine\":5,\"startLineOffset\":6,\"endLine\":7,\"endLineOffset\":8,\"hash\":\"hash2\"}," + "\"message\":\"Message 2\"}]},{\"locations\":[{\"filePath\":\"" + stringPath + "\"," + "\"textRange\":{\"startLine\":1,\"startLineOffset\":2,\"endLine\":3,\"endLineOffset\":4,\"hash\":\"hash1\"},\"message\":\"Message 1\"}]}]")); assertThat(flows).isEqualTo(List.of( new ServerTaintIssue.Flow(List.of( new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(1, 2, 3, 4, "hash1"), "Message 1"), new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(5, 6, 7, 8, "hash2"), "Message 2"))), new ServerTaintIssue.Flow(List.of( new ServerTaintIssue.ServerIssueLocation(path, new TextRangeWithHash(1, 2, 3, 4, "hash1"), "Message 1"))))); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/NewCodeDefinitionStorageTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.commons.NewCodeDefinition; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.sonarsource.sonarlint.core.serverconnection.storage.NewCodeDefinitionStorage.adapt; class NewCodeDefinitionStorageTests { @Test void shouldAdaptToProtobuf() { var days = adapt(NewCodeDefinition.withNumberOfDaysWithDate(30, 1000)); assertThat(days.getDays()).isEqualTo(30); assertThat(days.getThresholdDate()).isEqualTo(1000); var previousWithVersion = adapt(NewCodeDefinition.withPreviousVersion(1000, "1.0-SNAPSHOT")); assertThat(previousWithVersion.getVersion()).isEqualTo("1.0-SNAPSHOT"); assertThat(previousWithVersion.getThresholdDate()).isEqualTo(1000); var previousWithoutVersion = adapt(NewCodeDefinition.withPreviousVersion(1000, null)); assertThat(previousWithoutVersion.getVersion()).isEmpty(); assertThat(previousWithoutVersion.getThresholdDate()).isEqualTo(1000); var branch = adapt(NewCodeDefinition.withReferenceBranch("master")); assertThat(branch.getReferenceBranch()).isEqualTo("master"); } @Test void shouldAdaptFromProtobuf() { var daysProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.NUMBER_OF_DAYS) .setDays(30) .setThresholdDate(1000) .build(); var days = adapt(daysProto); assertThat(days.isSupported()).isTrue(); assertThat(days).isInstanceOf(NewCodeDefinition.NewCodeNumberOfDaysWithDate.class); assertThat(days.getThresholdDate().toEpochMilli()).isEqualTo(1000); assertThat(((NewCodeDefinition.NewCodeNumberOfDaysWithDate) days).getDays()).isEqualTo(30); var previousWithVersionProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.PREVIOUS_VERSION) .setVersion("1.0-SNAPSHOT") .setThresholdDate(1000) .build(); var previousWithVersion = adapt(previousWithVersionProto); assertThat(previousWithVersion.isSupported()).isTrue(); assertThat(previousWithVersion).isInstanceOf(NewCodeDefinition.NewCodePreviousVersion.class); assertThat(previousWithVersion.getThresholdDate().toEpochMilli()).isEqualTo(1000); assertThat(((NewCodeDefinition.NewCodePreviousVersion) previousWithVersion).getVersion()).isEqualTo("1.0-SNAPSHOT"); var previousWithoutVersionProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.PREVIOUS_VERSION) .setThresholdDate(1000) .build(); var previousWithoutVersion = adapt(previousWithoutVersionProto); assertThat(previousWithoutVersion.isSupported()).isTrue(); assertThat(previousWithoutVersion).isInstanceOf(NewCodeDefinition.NewCodePreviousVersion.class); assertThat(previousWithoutVersion.getThresholdDate().toEpochMilli()).isEqualTo(1000); assertThat(((NewCodeDefinition.NewCodePreviousVersion) previousWithoutVersion).getVersion()).isNull(); var branchProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.REFERENCE_BRANCH) .setReferenceBranch("master") .build(); var branch = adapt(branchProto); assertThat(branch.isSupported()).isFalse(); assertThat(branch).isInstanceOf(NewCodeDefinition.NewCodeReferenceBranch.class); assertThat(((NewCodeDefinition.NewCodeReferenceBranch) branch).getBranchName()).isEqualTo("master"); var analysisProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.SPECIFIC_ANALYSIS) .setThresholdDate(1000) .build(); var analysis = adapt(analysisProto); assertThat(analysis.isSupported()).isTrue(); assertThat(analysis).isInstanceOf(NewCodeDefinition.NewCodeSpecificAnalysis.class); assertThat(analysis.getThresholdDate().toEpochMilli()).isEqualTo(1000); var unknownProto = Sonarlint.NewCodeDefinition.newBuilder() .setMode(Sonarlint.NewCodeDefinitionMode.UNKNOWN) .build(); assertThatThrownBy(() -> adapt(unknownProto)).hasMessage("Unsupported mode: UNKNOWN"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/PluginsStorageTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertFalse; class PluginsStorageTests { @TempDir Path storageRoot; PluginsStorage underTest; @BeforeEach void setUp() { underTest = new PluginsStorage(storageRoot); } @Test void should_consider_storage_invalid_if_file_doesnt_exist() { assertFalse(underTest.isValid()); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/ProtobufFileUtilTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import com.google.protobuf.Parser; import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; class ProtobufFileUtilTests { private static final Sonarlint.PluginReferences SOME_MESSAGE = Sonarlint.PluginReferences.newBuilder().build(); private static final Parser SOME_PARSER = Sonarlint.PluginReferences.parser(); @Test void test_readFile_error() { var p = Paths.get("invalid_non_existing_file"); var thrown = assertThrows(StorageException.class, () -> ProtobufFileUtil.readFile(p, SOME_PARSER)); assertThat(thrown).hasMessageStartingWith("Failed to read file"); } @Test void test_writeFile_error() { var p = Paths.get("invalid", "non_existing", "file"); var thrown = assertThrows(StorageException.class, () -> ProtobufFileUtil.writeToFile(SOME_MESSAGE, p)); assertThat(thrown).hasMessageStartingWith("Unable to write protocol buffer data to file"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerFindingRepositoryTests.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRange; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.commons.storage.SonarLintDatabase; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; class ServerFindingRepositoryTests { private static final long INSTANT_TOLERANCE_MS = 1500; @RegisterExtension static SonarLintLogTester logTester = new SonarLintLogTester(); @TempDir Path tempDir; private ServerFindingRepository repo; private String branch; private Path filePath; private SonarLintDatabase db; @BeforeEach void setUp() { var storageRoot = tempDir.resolve("storage"); db = new SonarLintDatabase(storageRoot); repo = new ServerFindingRepository(db.dsl(), "conn-1", "project-1"); branch = "main"; filePath = Path.of("/file/path"); } @AfterEach void tearDown() { if (repo != null) { db.shutdown(); } } @Test void hotspots_replace_load_get_change_update_delete() { var h1 = hotspot("HOTSPOT_KEY_1", filePath, 1, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.MEDIUM, null); var h2 = hotspot("HOTSPOT_KEY_2", filePath, 2, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.HIGH, "john.doe"); repo.replaceAllHotspotsOfFile(branch, filePath, List.of(h1, h2)); var loaded = repo.loadHotspots(branch, filePath); assertThat(loaded).hasSize(2); var loadedH1 = loaded.stream().filter(h -> h.getKey().equals(h1.getKey())).findFirst().orElseThrow(); var loadedH2 = loaded.stream().filter(h -> h.getKey().equals(h2.getKey())).findFirst().orElseThrow(); assertHotspotEquals(h1, loadedH1); assertHotspotEquals(h2, loadedH2); var fetched = repo.getHotspot("HOTSPOT_KEY_2"); assertThat(fetched).isNotNull(); assertHotspotEquals(h2, fetched); assertThat(repo.changeHotspotStatus("HOTSPOT_KEY_1", HotspotReviewStatus.SAFE)).isTrue(); var afterStatus = repo.getHotspot("HOTSPOT_KEY_1"); var expectedAfterStatus = new ServerHotspot(h1.getId(), h1.getKey(), h1.getRuleKey(), h1.getMessage(), h1.getFilePath(), h1.getTextRange(), h1.getCreationDate(), HotspotReviewStatus.SAFE, h1.getVulnerabilityProbability(), h1.getAssignee()); assertHotspotEquals(expectedAfterStatus, afterStatus); repo.updateHotspot("HOTSPOT_KEY_2", hs -> { /* no-op */ }); var afterUpdate = repo.getHotspot("HOTSPOT_KEY_2"); assertHotspotEquals(h2, afterUpdate); repo.deleteHotspot("HOTSPOT_KEY_1"); assertThat(repo.getHotspot("HOTSPOT_KEY_1")).isNull(); } @Test void hotspots_replace_for_branch_load_get_change_update_delete() { var h1 = hotspot("HOTSPOT_KEY_1", filePath, 1, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.MEDIUM, null); var h2 = hotspot("HOTSPOT_KEY_2", filePath, 2, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.HIGH, "john.doe"); repo.replaceAllHotspotsOfBranch(branch, List.of(h1, h2), Set.of()); var loaded = repo.loadHotspots(branch, filePath); assertThat(loaded).hasSize(2); var loadedH1 = loaded.stream().filter(h -> h.getKey().equals(h1.getKey())).findFirst().orElseThrow(); var loadedH2 = loaded.stream().filter(h -> h.getKey().equals(h2.getKey())).findFirst().orElseThrow(); assertHotspotEquals(h1, loadedH1); assertHotspotEquals(h2, loadedH2); var fetched = repo.getHotspot("HOTSPOT_KEY_2"); assertThat(fetched).isNotNull(); assertHotspotEquals(h2, fetched); assertThat(repo.changeHotspotStatus("HOTSPOT_KEY_1", HotspotReviewStatus.SAFE)).isTrue(); var afterStatus = repo.getHotspot("HOTSPOT_KEY_1"); var expectedAfterStatus = new ServerHotspot(h1.getId(), h1.getKey(), h1.getRuleKey(), h1.getMessage(), h1.getFilePath(), h1.getTextRange(), h1.getCreationDate(), HotspotReviewStatus.SAFE, h1.getVulnerabilityProbability(), h1.getAssignee()); assertHotspotEquals(expectedAfterStatus, afterStatus); repo.updateHotspot("HOTSPOT_KEY_2", hs -> { /* no-op */ }); var afterUpdate = repo.getHotspot("HOTSPOT_KEY_2"); assertHotspotEquals(h2, afterUpdate); repo.deleteHotspot("HOTSPOT_KEY_1"); assertThat(repo.getHotspot("HOTSPOT_KEY_1")).isNull(); } @Test void server_dependency_risk() { var serverDependencyRisk1 = dependencyRisk(); repo.replaceAllDependencyRisksOfBranch(branch, List.of(serverDependencyRisk1)); var serverDependencyRisks = repo.loadDependencyRisks(branch); assertThat(serverDependencyRisks).hasSize(1); assertThat(serverDependencyRisks.get(0)).isEqualTo(serverDependencyRisk1); } @Test void issues_update() { var issueKey = "ISSUE_KEY"; var file = Path.of("/file/path"); var issue = rangeIssue(issueKey, file, new TextRangeWithHash(1, 10, 1, 20, "hash")); repo.replaceAllIssuesOfFile(branch, file, List.of(issue)); var loadedIssue = repo.getIssue(issueKey); assertRangeIssueEquals(issue, (RangeLevelServerIssue) loadedIssue); repo.updateIssue(issueKey, issueToUpdate -> issueToUpdate.setUserSeverity(IssueSeverity.MAJOR)); loadedIssue = repo.getIssue(issueKey); assertThat(loadedIssue.getUserSeverity()).isEqualTo(IssueSeverity.MAJOR); } @Test void replace_all_issues_of_branch() { var issueKey = "ISSUE_KEY"; var file = Path.of("/file/path"); var issue = rangeIssue(issueKey, file, new TextRangeWithHash(1, 10, 1, 20, "hash")); repo.replaceAllIssuesOfBranch(branch, List.of(issue), Set.of()); var loadedIssue = repo.getIssue(issueKey); assertRangeIssueEquals(issue, (RangeLevelServerIssue) loadedIssue); } @Test void was_ever_updated() { var issueKey = "ISSUE_KEY"; var file = Path.of("/file/path"); var issue = rangeIssue(issueKey, file, new TextRangeWithHash(1, 10, 1, 20, "hash")); repo.replaceAllIssuesOfBranch(branch, List.of(issue), Set.of()); assertThat(repo.wasEverUpdated()).isTrue(); } @Test void was_ever_updated_for_no_issues() { repo.replaceAllIssuesOfBranch(branch, List.of(), Set.of()); assertThat(repo.wasEverUpdated()).isTrue(); } @Test void was_never_updated() { assertThat(repo.wasEverUpdated()).isFalse(); } @Test void was_ever_updated_when_only_hotspots_synced() { var h1 = hotspot("HOTSPOT_KEY_1", filePath, 1, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.MEDIUM, null); repo.mergeHotspots(branch, List.of(h1), Set.of(), Instant.now(), Set.of()); assertThat(repo.wasEverUpdated()).isTrue(); } @Test void was_ever_updated_when_only_taints_synced() { var t1 = taint("TAINT_KEY_1", filePath); repo.mergeTaintIssues(branch, List.of(t1), Set.of(), Instant.now(), Set.of()); assertThat(repo.wasEverUpdated()).isTrue(); } @Test void was_ever_updated_when_several_branches() { var t1 = taint("TAINT_KEY_1", filePath); repo.mergeTaintIssues(branch, List.of(t1), Set.of(), Instant.now(), Set.of()); repo.mergeTaintIssues("otherbranch", List.of(t1), Set.of(), Instant.now(), Set.of()); assertThat(repo.wasEverUpdated()).isTrue(); } @Test void update_issue_resolution_status() { var issueKey = "ISSUE_KEY"; var file = Path.of("/file/path"); var issue = rangeIssue(issueKey, file, new TextRangeWithHash(1, 10, 1, 20, "hash")); repo.replaceAllIssuesOfFile(branch, file, List.of(issue)); assertThat(issue.isResolved()).isFalse(); var loadedIssue = repo.getIssue(issueKey); assertRangeIssueEquals(issue, (RangeLevelServerIssue) loadedIssue); var serverFinding = repo.updateIssueResolutionStatus(issueKey, false, true); assertThat(serverFinding).isPresent(); var repoIssue = repo.getIssue(issueKey); assertThat(repoIssue.isResolved()).isTrue(); } @Test void taints_replace_load_insert_delete_update() { var t1 = taint("TAINT_KEY_1", filePath); repo.replaceAllTaintsOfBranch(branch, List.of(t1), Set.of()); var loaded = repo.loadTaint(branch); assertThat(loaded).hasSize(1); assertTaintEquals(t1, loaded.get(0)); var t2 = taint("TAINT_KEY_2", filePath); repo.insert(branch, t2); loaded = repo.loadTaint(branch); assertThat(loaded).hasSize(2); var loadedT1 = loaded.stream().filter(t -> t.getSonarServerKey().equals("TAINT_KEY_1")).findFirst().orElseThrow(); var loadedT2 = loaded.stream().filter(t -> t.getSonarServerKey().equals("TAINT_KEY_2")).findFirst().orElseThrow(); assertTaintEquals(t1, loadedT1); assertTaintEquals(t2, loadedT2); var deletedId = repo.deleteTaintIssueBySonarServerKey("TAINT_KEY_2"); assertThat(deletedId).isPresent(); loaded = repo.loadTaint(branch); assertThat(loaded).hasSize(1); assertTaintEquals(t1, loaded.get(0)); assertThat(repo.updateTaintIssueBySonarServerKey("TAINT_KEY_1", t -> { /* no-op */ })).isPresent(); var afterUpdate = repo.loadTaint(branch).get(0); assertTaintEquals(t1, afterUpdate); } @Test void merge_issues_removes_closed_and_upserts() { var newIssue = lineIssue("ISSUE_KEY_4", filePath, 2); repo.mergeIssues(branch, List.of(newIssue), Set.of("ISSUE_KEY_1"), Instant.now(), Set.of()); var afterMerge = repo.load(branch, filePath); assertThat(afterMerge.stream().anyMatch(i -> i.getKey().equals("ISSUE_KEY_1"))).isFalse(); assertThat(afterMerge.stream().anyMatch(i -> i.getKey().equals("ISSUE_KEY_4"))).isTrue(); } @Test void merge_taints_removes_closed_and_upserts() { var t3 = new ServerTaintIssue(UUID.randomUUID(), "TAINT_KEY_3", false, null, "rule", "msg", filePath, Instant.now(), IssueSeverity.MINOR, RuleType.CODE_SMELL, null, null, null, Map.of(), List.of()); repo.mergeTaintIssues(branch, List.of(t3), Set.of("TAINT_KEY_1"), Instant.now(), Set.of()); var afterMerge = repo.loadTaint(branch); assertThat(afterMerge.stream().anyMatch(t -> t.getSonarServerKey().equals("TAINT_KEY_1"))).isFalse(); assertThat(afterMerge.stream().anyMatch(t -> t.getSonarServerKey().equals("TAINT_KEY_3"))).isTrue(); } @Test void merge_hotspots_removes_closed_and_upserts() { var h3 = new ServerHotspot(UUID.randomUUID(), "HOTSPOT_KEY_3", "rule", "msg", filePath, new TextRange(4, 0, 4, 1), Instant.now(), HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.LOW, null); repo.mergeHotspots(branch, List.of(h3), Set.of("HOTSPOT_KEY_2"), Instant.now(), Set.of()); var afterMerge = repo.loadHotspots(branch, filePath); assertThat(afterMerge.stream().anyMatch(h -> h.getKey().equals("HOTSPOT_KEY_2"))).isFalse(); assertThat(afterMerge.stream().anyMatch(h -> h.getKey().equals("HOTSPOT_KEY_3"))).isTrue(); } @Test void branch_metadata_is_stored_during_merges() { // perform one merge for each type to set metadata repo.mergeIssues(branch, List.of(lineIssue("ISSUE_KEY_X", filePath, 1)), Set.of(), Instant.now(), Set.of()); repo.mergeTaintIssues(branch, List.of(taint("TAINT_KEY_X", filePath)), Set.of(), Instant.now(), Set.of()); repo.mergeHotspots(branch, List.of(hotspot("HOTSPOT_KEY_X", filePath, 1, HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.LOW, null)), Set.of(), Instant.now(), Set.of()); assertThat(repo.getLastIssueSyncTimestamp(branch)).isPresent(); assertThat(repo.getLastTaintSyncTimestamp(branch)).isPresent(); assertThat(repo.getLastHotspotSyncTimestamp(branch)).isPresent(); assertThat(repo.getLastIssueEnabledLanguages(branch)).isEmpty(); assertThat(repo.getLastHotspotEnabledLanguages(branch)).isEmpty(); assertThat(repo.getLastTaintEnabledLanguages(branch)).isEmpty(); } // Helpers private static void assertInstantsClose(Instant expected, Instant actual) { long diff = Math.abs(expected.toEpochMilli() - actual.toEpochMilli()); assertThat(diff).isLessThan(INSTANT_TOLERANCE_MS); } private static void assertTextRangeEquals(TextRange expected, TextRange actual) { assertThat(actual.getStartLine()).isEqualTo(expected.getStartLine()); assertThat(actual.getStartLineOffset()).isEqualTo(expected.getStartLineOffset()); assertThat(actual.getEndLine()).isEqualTo(expected.getEndLine()); assertThat(actual.getEndLineOffset()).isEqualTo(expected.getEndLineOffset()); } private static void assertTextRangeWithHashEquals(TextRangeWithHash expected, TextRangeWithHash actual) { assertThat(actual.getHash()).isEqualTo(expected.getHash()); assertTextRangeEquals(expected, actual); } private static void assertHotspotEquals(ServerHotspot expected, ServerHotspot actual) { assertThat(actual.getId()).isEqualTo(expected.getId()); assertThat(actual.getKey()).isEqualTo(expected.getKey()); assertThat(actual.getRuleKey()).isEqualTo(expected.getRuleKey()); assertThat(actual.getMessage()).isEqualTo(expected.getMessage()); assertThat(actual.getFilePath()).isEqualTo(expected.getFilePath()); assertTextRangeEquals(expected.getTextRange(), actual.getTextRange()); assertInstantsClose(expected.getCreationDate(), actual.getCreationDate()); assertThat(actual.getStatus()).isEqualTo(expected.getStatus()); assertThat(actual.getVulnerabilityProbability()).isEqualTo(expected.getVulnerabilityProbability()); assertThat(actual.getAssignee()).isEqualTo(expected.getAssignee()); } private static void assertTaintEquals(ServerTaintIssue expected, ServerTaintIssue actual) { assertThat(actual.getId()).isEqualTo(expected.getId()); assertThat(actual.getSonarServerKey()).isEqualTo(expected.getSonarServerKey()); assertThat(actual.getRuleKey()).isEqualTo(expected.getRuleKey()); assertThat(actual.getMessage()).isEqualTo(expected.getMessage()); assertThat(actual.getFilePath()).isEqualTo(expected.getFilePath()); assertInstantsClose(expected.getCreationDate(), actual.getCreationDate()); assertThat(actual.getSeverity()).isEqualTo(expected.getSeverity()); assertThat(actual.getType()).isEqualTo(expected.getType()); if (expected.getTextRange() != null && actual.getTextRange() != null) { assertTextRangeWithHashEquals(expected.getTextRange(), actual.getTextRange()); } else { assertThat(actual.getTextRange()).isNull(); assertThat(expected.getTextRange()).isNull(); } } private static void assertRangeIssueEquals(RangeLevelServerIssue expected, RangeLevelServerIssue actual) { assertThat(actual.getId()).isEqualTo(expected.getId()); assertThat(actual.getKey()).isEqualTo(expected.getKey()); assertThat(actual.getRuleKey()).isEqualTo(expected.getRuleKey()); assertThat(actual.getMessage()).isEqualTo(expected.getMessage()); assertThat(actual.getFilePath()).isEqualTo(expected.getFilePath()); assertInstantsClose(expected.getCreationDate(), actual.getCreationDate()); assertThat(actual.getUserSeverity()).isEqualTo(expected.getUserSeverity()); assertThat(actual.getType()).isEqualTo(expected.getType()); assertTextRangeWithHashEquals(expected.getTextRange(), actual.getTextRange()); } private static ServerHotspot hotspot(String key, Path file, int startLine, HotspotReviewStatus status, VulnerabilityProbability prob, String assignee) { return new ServerHotspot(UUID.randomUUID(), key, "hotspot:rule-" + key, "Hotspot Message " + key, file, new TextRange(startLine, 0, startLine, 10), Instant.now(), status, prob, assignee); } private static ServerTaintIssue taint(String key, Path file) { return new ServerTaintIssue(UUID.randomUUID(), key, false, null, "java:S" + Math.abs(key.hashCode() % 1000), "Taint message " + key, file, Instant.now(), IssueSeverity.MAJOR, RuleType.SECURITY_HOTSPOT, new TextRangeWithHash(3, 1, 3, 5, "hash-" + key), null, null, Map.of(), List.of()); } private static LineLevelServerIssue lineIssue(String key, Path file, int line) { return new LineLevelServerIssue(UUID.randomUUID(), key, false, null, "rule", "msg", "h", file, Instant.now(), IssueSeverity.MINOR, RuleType.CODE_SMELL, line, Map.of()); } private static RangeLevelServerIssue rangeIssue(String key, Path file, TextRangeWithHash range) { return new RangeLevelServerIssue(UUID.randomUUID(), key, false, IssueStatus.ACCEPT, "ruleKey", "message", file, Instant.now(), IssueSeverity.MINOR, RuleType.CODE_SMELL, range, Map.of()); } private static ServerDependencyRisk dependencyRisk() { return new ServerDependencyRisk(UUID.randomUUID(), ServerDependencyRisk.Type.VULNERABILITY, ServerDependencyRisk.Severity.HIGH, ServerDependencyRisk.SoftwareQuality.SECURITY, ServerDependencyRisk.Status.ACCEPT, "package", "version", "vulnerabilityId", "cvssScore", List.of(ServerDependencyRisk.Transition.REOPEN, ServerDependencyRisk.Transition.CONFIRM)); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerHotspotFixtures.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.time.Instant; import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus; import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot; public class ServerHotspotFixtures { public static ServerHotspot aServerHotspot() { return aServerHotspot("key", Path.of("file/path")); } public static ServerHotspot aServerHotspot(String key) { return aServerHotspot(key, Path.of("file/path")); } public static ServerHotspot aServerHotspot(String key, Path filePath) { return new ServerHotspot( key, "repo:key", "message", filePath, new TextRangeWithHash(1, 2, 3, 4, ""), Instant.now(), HotspotReviewStatus.TO_REVIEW, VulnerabilityProbability.HIGH, "test@user.com"); } } ================================================ FILE: backend/server-connection/src/test/java/org/sonarsource/sonarlint/core/serverconnection/storage/ServerIssueFixtures.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.serverconnection.storage; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.UUID; import org.sonarsource.sonarlint.core.commons.CleanCodeAttribute; import org.sonarsource.sonarlint.core.commons.ImpactSeverity; import org.sonarsource.sonarlint.core.commons.IssueSeverity; import org.sonarsource.sonarlint.core.commons.IssueStatus; import org.sonarsource.sonarlint.core.commons.RuleType; import org.sonarsource.sonarlint.core.commons.SoftwareQuality; import org.sonarsource.sonarlint.core.commons.api.TextRangeWithHash; import org.sonarsource.sonarlint.core.serverconnection.issues.FileLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerDependencyRisk; import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue; public class ServerIssueFixtures { public static LineLevelServerIssue aBatchServerIssue() { return new LineLevelServerIssue( "key", true, IssueStatus.WONT_FIX, "repo:key", "message", "hash", Path.of("file/path"), Instant.now(), IssueSeverity.MINOR, RuleType.BUG, 1, Map.of(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH)); } public static FileLevelServerIssue aFileLevelServerIssue() { return new FileLevelServerIssue( "key", true, IssueStatus.WONT_FIX, "repo:key", "message", Path.of("file/path"), Instant.now(), IssueSeverity.MINOR, RuleType.BUG, Map.of(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH)); } public static RangeLevelServerIssue aServerIssue() { return new RangeLevelServerIssue( "key", true, IssueStatus.WONT_FIX, "repo:key", "message", Path.of("file/path"), Instant.now(), IssueSeverity.MINOR, RuleType.BUG, new TextRangeWithHash(1, 2, 3, 4, "ab12"), Map.of(SoftwareQuality.MAINTAINABILITY, ImpactSeverity.HIGH)); } public static ServerTaintIssue aServerTaintIssue() { return new ServerTaintIssue( UUID.randomUUID(), "key", false, null, "repo:key", "message", Path.of("file/path"), Instant.now(), IssueSeverity.MINOR, RuleType.VULNERABILITY, new TextRangeWithHash(1, 2, 3, 4, "ab12"), "context", CleanCodeAttribute.TRUSTWORTHY, Map.of(SoftwareQuality.SECURITY, ImpactSeverity.HIGH), List.of(aServerTaintIssueFlow())); } public static ServerDependencyRisk aServerDependencyRisk() { return new ServerDependencyRisk( UUID.randomUUID(), ServerDependencyRisk.Type.VULNERABILITY, ServerDependencyRisk.Severity.HIGH, ServerDependencyRisk.SoftwareQuality.SECURITY, ServerDependencyRisk.Status.OPEN, "com.example.vulnerable", "1.0.0", "CVE-1234", "7.5", List.of( ServerDependencyRisk.Transition.CONFIRM, ServerDependencyRisk.Transition.REOPEN)); } private static ServerTaintIssue.Flow aServerTaintIssueFlow() { return new ServerTaintIssue.Flow(List.of(aServerTaintIssueFlowLocation())); } private static ServerTaintIssue.ServerIssueLocation aServerTaintIssueFlowLocation() { return new ServerTaintIssue.ServerIssueLocation( Path.of("file/path"), new TextRangeWithHash(5, 6, 7, 8, "rangeHash"), "message"); } } ================================================ FILE: backend/server-connection/src/test/java/testutils/MockWebServerExtensionWithProtobuf.java ================================================ /* * SonarLint Core - Server Connection * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package testutils; import com.google.protobuf.Message; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Iterator; import javax.annotation.Nullable; import mockwebserver3.MockResponse; import okio.Buffer; import org.sonarsource.sonarlint.core.commons.testutils.MockWebServerExtension; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.serverapi.EndpointParams; import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper; import static org.junit.jupiter.api.Assertions.fail; public class MockWebServerExtensionWithProtobuf extends MockWebServerExtension { public void addProtobufResponse(String path, Message m) { try (var b = new Buffer()) { m.writeTo(b.outputStream()); responsesByPath.put(path, new MockResponse.Builder().body(b).build()); } catch (IOException e) { fail(e); } } public void addProtobufResponseDelimited(String path, Message... m) { try (var b = new Buffer()) { writeMessages(b.outputStream(), Arrays.asList(m).iterator()); responsesByPath.put(path, new MockResponse.Builder().body(b).build()); } } public static void writeMessages(OutputStream output, Iterator messages) { while (messages.hasNext()) { writeMessage(output, messages.next()); } } public static void writeMessage(OutputStream output, T message) { try { message.writeDelimitedTo(output); } catch (IOException e) { throw new IllegalStateException("failed to write message: " + message, e); } } public ServerApiHelper serverApiHelper() { return serverApiHelper(null); } public ServerApiHelper serverApiHelper(@Nullable String organizationKey) { return new ServerApiHelper(endpointParams(organizationKey), HttpClientProvider.forTesting().getHttpClientWithoutAuth()); } public EndpointParams endpointParams() { return endpointParams(null); } public EndpointParams endpointParams(@Nullable String organizationKey) { return new EndpointParams(url("/"), url("/"), organizationKey != null, organizationKey); } } ================================================ FILE: backend/server-connection/src/test/resources/logback-test.xml ================================================ ================================================ FILE: backend/telemetry/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-backend-parent 11.2-SNAPSHOT ../pom.xml sonarlint-telemetry SonarLint Core - Telemetry Manage telemetry com.google.code.findbugs jsr305 provided ${project.groupId} sonarlint-commons ${project.version} ${project.groupId} sonarlint-http ${project.version} org.sonarsource.sonarlint.core sonarlint-rpc-protocol ${project.version} com.google.code.gson gson org.apache.commons commons-lang3 org.springframework spring-context provided org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test org.assertj assertj-core test org.mockito mockito-core test org.wiremock wiremock-jetty12 test ch.qos.logback logback-classic test org.awaitility awaitility test conditionally-add-commons-tests-if-tests-not-skipped maven.test.skip !true ${project.groupId} sonarlint-commons ${project.version} tests test-jar test ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/InternalDebug.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; /** * Telemetry issues are silently ignored to not annoy users. In order to ease detection of issues, people (usually SonarSourcers) can define * this env variable to see telemetry errors in their logs. * */ public class InternalDebug { static final String INTERNAL_DEBUG_ENV = "SONARLINT_INTERNAL_DEBUG"; private static boolean isEnabled = "true".equals(System.getenv(INTERNAL_DEBUG_ENV)); private InternalDebug() { // utility class, forbidden constructor } public static boolean isEnabled() { return isEnabled; } // For testing public static void setEnabled(boolean isEnabled) { InternalDebug.isEnabled = isEnabled; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryAnalysisReportingCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; public class TelemetryAnalysisReportingCounter { private int analysisReportingCount; public TelemetryAnalysisReportingCounter() { } public TelemetryAnalysisReportingCounter(int analysisReportingTriggered) { this.analysisReportingCount = analysisReportingTriggered; } public int getAnalysisReportingCount() { return analysisReportingCount; } public void incrementAnalysisReportingCount() { this.analysisReportingCount++; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryAnalyzerPerformance.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; public class TelemetryAnalyzerPerformance { private static final TreeMap INTERVALS; private int analysisCount; static { INTERVALS = new TreeMap<>(); INTERVALS.put(300, "0-300"); INTERVALS.put(500, "300-500"); INTERVALS.put(1000, "500-1000"); INTERVALS.put(2000, "1000-2000"); INTERVALS.put(4000, "2000-4000"); INTERVALS.put(Integer.MAX_VALUE, "4000+"); } private final Map frequencies; public TelemetryAnalyzerPerformance() { frequencies = new LinkedHashMap<>(); INTERVALS.forEach((k, v) -> frequencies.put(v, 0)); } public void registerAnalysis(int analysisTimeMs) { var entry = INTERVALS.higherEntry(analysisTimeMs); if (entry != null) { frequencies.compute(entry.getValue(), (k, v) -> v != null ? (v + 1) : 1); analysisCount++; } } public Map frequencies() { return frequencies; } public int analysisCount() { return analysisCount; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryConnectionAttributes.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import javax.annotation.Nullable; public record TelemetryConnectionAttributes( @Nullable String userId, @Nullable String serverId, @Nullable String organizationId ) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFindingsFilteredCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; public class TelemetryFindingsFilteredCounter { private int findingsFilteredCount; public TelemetryFindingsFilteredCounter() { } public TelemetryFindingsFilteredCounter(int findingsFilteredCount) { this.findingsFilteredCount = findingsFilteredCount; } public int getFindingsFilteredCount() { return findingsFilteredCount; } public void incrementFindingsFilteredCount() { this.findingsFilteredCount++; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionReceivedCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; public record TelemetryFixSuggestionReceivedCounter(AiSuggestionSource aiSuggestionsSource, int snippetsCount, boolean wasGeneratedFromIde) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionResolvedStatus.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; public class TelemetryFixSuggestionResolvedStatus { @Nullable private FixSuggestionStatus fixSuggestionResolvedStatus; @Nullable private final Integer fixSuggestionResolvedSnippetIndex; public TelemetryFixSuggestionResolvedStatus(@Nullable FixSuggestionStatus fixSuggestionResolvedStatus, @Nullable Integer fixSuggestionResolvedSnippetIndex) { this.fixSuggestionResolvedStatus = fixSuggestionResolvedStatus; this.fixSuggestionResolvedSnippetIndex = fixSuggestionResolvedSnippetIndex; } public FixSuggestionStatus getFixSuggestionResolvedStatus() { return fixSuggestionResolvedStatus; } @Nullable public Integer getFixSuggestionResolvedSnippetIndex() { return fixSuggestionResolvedSnippetIndex; } public void setFixSuggestionResolvedStatus(FixSuggestionStatus fixSuggestionResolvedStatus) { this.fixSuggestionResolvedStatus = fixSuggestionResolvedStatus; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHelpAndFeedbackCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; public class TelemetryHelpAndFeedbackCounter { private int helpAndFeedbackLinkClickedCount; public TelemetryHelpAndFeedbackCounter() { } public TelemetryHelpAndFeedbackCounter(int helpAndFeedbackLinkClicked) { this.helpAndFeedbackLinkClickedCount = helpAndFeedbackLinkClicked; } public int getHelpAndFeedbackLinkClickedCount() { return helpAndFeedbackLinkClickedCount; } public void incrementHelpAndFeedbackLinkClickedCount() { this.helpAndFeedbackLinkClickedCount++; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClient.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import com.google.common.annotations.VisibleForTesting; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.SystemUtils; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto; import org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresBuilder; import org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresPayload; import org.sonarsource.sonarlint.core.telemetry.payload.HotspotPayload; import org.sonarsource.sonarlint.core.telemetry.payload.IssuePayload; import org.sonarsource.sonarlint.core.telemetry.payload.ShareConnectedModePayload; import org.sonarsource.sonarlint.core.telemetry.payload.ShowHotspotPayload; import org.sonarsource.sonarlint.core.telemetry.payload.ShowIssuePayload; import org.sonarsource.sonarlint.core.telemetry.payload.TaintVulnerabilitiesPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryHelpAndFeedbackPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryRulesPayload; import org.sonarsource.sonarlint.core.telemetry.payload.cayc.CleanAsYouCodePayload; import org.sonarsource.sonarlint.core.telemetry.payload.cayc.NewCodeFocusPayload; import org.springframework.beans.factory.annotation.Qualifier; public class TelemetryHttpClient { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final String product; private final String version; private final String ideVersion; private final String platform; private final String architecture; private final HttpClient client; private final String endpoint; private final Map additionalAttributes; public TelemetryHttpClient(InitializeParams initializeParams, HttpClientProvider httpClientProvider, @Qualifier("telemetryEndpoint") String telemetryEndpoint) { TelemetryClientConstantAttributesDto attributes = initializeParams.getTelemetryConstantAttributes(); this.product = attributes.getProductName(); this.version = attributes.getProductVersion(); this.ideVersion = attributes.getIdeVersion(); this.platform = SystemUtils.OS_NAME; this.architecture = SystemUtils.OS_ARCH; this.client = httpClientProvider.getHttpClientWithoutAuth(); this.endpoint = telemetryEndpoint; this.additionalAttributes = attributes.getAdditionalAttributes(); } void upload(TelemetryLocalStorage data, TelemetryLiveAttributes telemetryLiveAttributes) { try { sendPost(createPayload(data, telemetryLiveAttributes)); } catch (Throwable catchEmAll) { if (InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry data", catchEmAll); } } try { sendMetricsPostIfNeeded(new TelemetryMeasuresBuilder(platform, product, data, telemetryLiveAttributes).build()); } catch (Throwable catchEmAll) { if (InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry metrics data", catchEmAll); } } } void optOut(TelemetryLocalStorage data, TelemetryLiveAttributes telemetryLiveAttributes) { try { sendDelete(createPayload(data, telemetryLiveAttributes)); } catch (Throwable catchEmAll) { if (InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry opt-out", catchEmAll); } } } private TelemetryPayload createPayload(TelemetryLocalStorage data, TelemetryLiveAttributes telemetryLiveAttrs) { var systemTime = OffsetDateTime.now(); var daysSinceInstallation = data.installTime().until(systemTime, ChronoUnit.DAYS); var analyzers = TelemetryUtils.toPayload(data.analyzers()); var notifications = TelemetryUtils.toPayload(telemetryLiveAttrs.isDevNotificationsDisabled(), data.notifications()); var showHotspotPayload = new ShowHotspotPayload(data.showHotspotRequestsCount()); var showIssuePayload = new ShowIssuePayload(data.getShowIssueRequestsCount()); var hotspotPayload = new HotspotPayload(data.openHotspotInBrowserCount(), data.hotspotStatusChangedCount()); var taintVulnerabilitiesPayload = new TaintVulnerabilitiesPayload(data.taintVulnerabilitiesInvestigatedLocallyCount(), data.taintVulnerabilitiesInvestigatedRemotelyCount()); var issuePayload = new IssuePayload(data.issueStatusChangedRuleKeys(), data.issueStatusChangedCount()); var jre = System.getProperty("java.version"); var telemetryRulesPayload = new TelemetryRulesPayload(telemetryLiveAttrs.getNonDefaultEnabledRules(), telemetryLiveAttrs.getDefaultDisabledRules(), data.getRaisedIssuesRules(), data.getQuickFixesApplied()); var helpAndFeedbackPayload = new TelemetryHelpAndFeedbackPayload(data.getHelpAndFeedbackLinkClickedCounter()); var fixSuggestionPayload = TelemetryUtils.toFixSuggestionResolvedPayload( data.getFixSuggestionReceivedCounter(), data.getFixSuggestionResolved() ); var countIssuesWithPossibleAiFixFromIde = data.getCountIssuesWithPossibleAiFixFromIde(); var cleanAsYouCodePayload = new CleanAsYouCodePayload(new NewCodeFocusPayload(data.isFocusOnNewCode(), data.getCodeFocusChangedCount())); ShareConnectedModePayload shareConnectedModePayload; if (telemetryLiveAttrs.usesConnectedMode()) { shareConnectedModePayload = new ShareConnectedModePayload(data.getManualAddedBindingsCount(), data.getImportedAddedBindingsCount(), data.getAutoAddedBindingsCount(), data.getExportedConnectedModeCount()); } else { shareConnectedModePayload = new ShareConnectedModePayload(null, null, null, null); } var mergedAdditionalAttributes = new HashMap<>(telemetryLiveAttrs.getAdditionalAttributes()); mergedAdditionalAttributes.putAll(additionalAttributes); return new TelemetryPayload(daysSinceInstallation, data.numUseDays(), product, version, ideVersion, platform, architecture, telemetryLiveAttrs.usesConnectedMode(), telemetryLiveAttrs.usesSonarCloud(), systemTime, data.installTime(), platform, jre, telemetryLiveAttrs.getNodeVersion(), analyzers, notifications, showHotspotPayload, showIssuePayload, taintVulnerabilitiesPayload, telemetryRulesPayload, hotspotPayload, issuePayload, helpAndFeedbackPayload, fixSuggestionPayload, countIssuesWithPossibleAiFixFromIde, cleanAsYouCodePayload, shareConnectedModePayload, mergedAdditionalAttributes); } private void sendPost(TelemetryPayload payload) { logTelemetryPayload(payload); var responseCompletableFuture = client.postAsync(endpoint, HttpClient.JSON_CONTENT_TYPE, payload.toJson()); handleTelemetryResponse(responseCompletableFuture, "data"); } private void sendMetricsPostIfNeeded(TelemetryMeasuresPayload payload) { if (!payload.hasMetrics()) { // No metrics to send if (isTelemetryLogEnabled()) { LOG.info("Not sending empty telemetry metrics payload."); } return; } logTelemetryMetricsPayload(payload); var responseCompletableFuture = client.postAsync(endpoint + "/metrics", HttpClient.JSON_CONTENT_TYPE, payload.toJson()); handleTelemetryResponse(responseCompletableFuture, "data"); } private void logTelemetryPayload(TelemetryPayload payload) { if (isTelemetryLogEnabled()) { LOG.info("Sending telemetry payload."); LOG.info(payload.toJson()); } } private void logTelemetryMetricsPayload(TelemetryMeasuresPayload payload) { if (isTelemetryLogEnabled()) { LOG.info("Sending telemetry metrics payload."); LOG.info(payload.toJson()); } } private void sendDelete(TelemetryPayload payload) { var responseCompletableFuture = client.deleteAsync(endpoint, HttpClient.JSON_CONTENT_TYPE, payload.toJson()); handleTelemetryResponse(responseCompletableFuture, "opt-out"); } private static void handleTelemetryResponse(CompletableFuture responseCompletableFuture, String uploadType) { responseCompletableFuture.thenAccept(response -> { if (!response.isSuccessful() && InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry {}: {}", uploadType, response.toString()); } }).exceptionally(exception -> { if (InternalDebug.isEnabled()) { LOG.error(String.format("Failed to upload telemetry %s", uploadType), exception); } return null; }); } @VisibleForTesting boolean isTelemetryLogEnabled(){ return Boolean.parseBoolean(System.getenv("SONARLINT_TELEMETRY_LOG")); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLiveAttributes.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; public class TelemetryLiveAttributes { private final TelemetryServerAttributes serverAttributes; private final TelemetryClientLiveAttributesResponse clientAttributes; public TelemetryLiveAttributes(TelemetryServerAttributes serverAttributes, TelemetryClientLiveAttributesResponse clientAttributes) { this.serverAttributes = serverAttributes; this.clientAttributes = clientAttributes; } public boolean usesConnectedMode() { return serverAttributes.usesConnectedMode(); } public boolean usesSonarCloud() { return serverAttributes.usesSonarCloud(); } public int countChildBindings() { return serverAttributes.childBindingCount(); } public int countSonarQubeServerBindings() { return serverAttributes.sonarQubeServerBindingCount(); } public int countSonarQubeCloudEUBindings() { return serverAttributes.sonarQubeCloudEUBindingCount(); } public int countSonarQubeCloudUSBindings() { return serverAttributes.sonarQubeCloudUSBindingCount(); } public boolean isDevNotificationsDisabled() { return serverAttributes.devNotificationsDisabled(); } public List getNonDefaultEnabledRules() { return serverAttributes.nonDefaultEnabledRules(); } public List getDefaultDisabledRules() { return serverAttributes.defaultDisabledRules(); } @Nullable public String getNodeVersion() { return serverAttributes.nodeVersion(); } public List getConnectionsAttributes() { return serverAttributes.connectionsAttributes(); } public Map getAdditionalAttributes() { return clientAttributes.getAdditionalAttributes(); } public boolean hasJoinedIdeLabs() { return clientAttributes.hasJoinedIdeLabs(); } public boolean hasEnabledIdeLabs() { return clientAttributes.hasEnabledIdeLabs(); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorage.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.storage.local.LocalStorage; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingType; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import static java.time.temporal.ChronoUnit.DAYS; public class TelemetryLocalStorage implements LocalStorage { @Deprecated private LocalDate installDate; private LocalDate lastUseDate; private LocalDateTime lastUploadDateTime; private OffsetDateTime installTime; private long numUseDays; private boolean enabled; private final Map analyzers; private final Map notificationsCountersByEventType; private int showHotspotRequestsCount; private int showIssueRequestsCount; private int openHotspotInBrowserCount; private int taintVulnerabilitiesInvestigatedLocallyCount; private int taintVulnerabilitiesInvestigatedRemotelyCount; private int hotspotStatusChangedCount; private final Set issueStatusChangedRuleKeys; private int issueStatusChangedCount; private final Set raisedIssuesRules; private final Set quickFixesApplied; private final Map quickFixCountByRuleKey; private final Map helpAndFeedbackLinkClickedCount; private final Map analysisReportingCountersByType; private final Map findingsFilteredCountersByType; private final Map fixSuggestionReceivedCounter; private final Map> fixSuggestionResolved; private final Map calledToolsByName; private final Set issuesUuidAiFixableSeen; private boolean isFocusOnNewCode; private int codeFocusChangedCount; private int manualAddedBindingsCount; private int importedAddedBindingsCount; private int autoAddedBindingsCount; private int exportedConnectedModeCount; private int newBindingsPropertiesFileCount; private int newBindingsRemoteUrlCount; private int newBindingsProjectNameCount; private int newBindingsSharedConfigurationCount; private int suggestedRemoteBindingsCount; private long newIssuesFoundCount; private long issuesFixedCount; private int biggestNumberOfFilesInConfigScope; private long listingTimeForBiggestNumberConfigScopeFiles; private long longestListingTimeForConfigScopeFiles; private int numberOfFilesForLongestFilesListingTimeConfigScope; private int taintInvestigatedLocallyCount; private int taintInvestigatedRemotelyCount; private int hotspotInvestigatedLocallyCount; private int hotspotInvestigatedRemotelyCount; private int issueInvestigatedLocallyCount; private int dependencyRiskInvestigatedRemotelyCount; private int dependencyRiskInvestigatedLocallyCount; private boolean isAutomaticAnalysisEnabled; private int automaticAnalysisToggledCount; private int mcpServerConfigurationRequestedCount; private int mcpRuleFileRequestedCount; private boolean isMcpIntegrationEnabled; @Nullable private McpTransportMode mcpTransportModeUsed; private final Map labsLinkClickedCount; private final Map labsFeedbackLinkClickedCount; private final Map aiHooksInstalledCount; private final Map campaignsShown; private final Map campaignsResolutions; private int supportedLanguagesPanelOpenedCount; private int supportedLanguagesPanelCtaClickedCount; TelemetryLocalStorage() { enabled = true; installTime = OffsetDateTime.now(); analyzers = new LinkedHashMap<>(); notificationsCountersByEventType = new LinkedHashMap<>(); issueStatusChangedRuleKeys = new HashSet<>(); raisedIssuesRules = new HashSet<>(); quickFixesApplied = new HashSet<>(); quickFixCountByRuleKey = new LinkedHashMap<>(); helpAndFeedbackLinkClickedCount = new LinkedHashMap<>(); analysisReportingCountersByType = new LinkedHashMap<>(); findingsFilteredCountersByType = new LinkedHashMap<>(); fixSuggestionReceivedCounter = new LinkedHashMap<>(); fixSuggestionResolved = new LinkedHashMap<>(); issuesUuidAiFixableSeen = new HashSet<>(); calledToolsByName = new HashMap<>(); labsLinkClickedCount = new HashMap<>(); labsFeedbackLinkClickedCount = new HashMap<>(); aiHooksInstalledCount = new EnumMap<>(AiAgent.class); campaignsShown = new HashMap<>(); campaignsResolutions = new HashMap<>(); } public Collection getRaisedIssuesRules() { return raisedIssuesRules; } public void addReportedRules(Set reportedRuleKeys) { this.raisedIssuesRules.addAll(reportedRuleKeys); } public Collection getQuickFixesApplied() { return quickFixesApplied; } public void addQuickFixAppliedForRule(String ruleKey) { markSonarLintAsUsedToday(); this.quickFixesApplied.add(ruleKey); var currentCountForKey = this.quickFixCountByRuleKey.getOrDefault(ruleKey, 0); this.quickFixCountByRuleKey.put(ruleKey, currentCountForKey + 1); } public Map getQuickFixCountByRuleKey() { return quickFixCountByRuleKey; } @Deprecated void setInstallDate(LocalDate date) { this.installDate = date; } @Deprecated public LocalDate installDate() { return installDate; } public OffsetDateTime installTime() { return installTime; } public void setInstallTime(OffsetDateTime installTime) { this.installTime = installTime; } void setLastUseDate(@Nullable LocalDate date) { this.lastUseDate = date; } @CheckForNull public LocalDate lastUseDate() { return lastUseDate; } public Map analyzers() { return analyzers; } public Map notifications() { return notificationsCountersByEventType; } public Map getHelpAndFeedbackLinkClickedCounter() { return helpAndFeedbackLinkClickedCount; } public Map getAnalysisReportingCountersByType() { return analysisReportingCountersByType; } public Map getFindingsFilteredCountersByType() { return findingsFilteredCountersByType; } public Map getFixSuggestionReceivedCounter() { return fixSuggestionReceivedCounter; } public Map> getFixSuggestionResolved() { return fixSuggestionResolved; } public int getCountIssuesWithPossibleAiFixFromIde() { return issuesUuidAiFixableSeen.size(); } public boolean isFocusOnNewCode() { return isFocusOnNewCode; } public int getCodeFocusChangedCount() { return codeFocusChangedCount; } void setLastUploadTime() { setLastUploadTime(LocalDateTime.now()); } void setLastUploadTime(@Nullable LocalDateTime dateTime) { this.lastUploadDateTime = dateTime; } @CheckForNull public LocalDateTime lastUploadTime() { return lastUploadDateTime; } void setNumUseDays(long numUseDays) { this.numUseDays = numUseDays; } void clearAfterPing() { analyzers.clear(); notificationsCountersByEventType.clear(); showHotspotRequestsCount = 0; showIssueRequestsCount = 0; openHotspotInBrowserCount = 0; taintVulnerabilitiesInvestigatedLocallyCount = 0; taintVulnerabilitiesInvestigatedRemotelyCount = 0; hotspotStatusChangedCount = 0; issueStatusChangedRuleKeys.clear(); issueStatusChangedCount = 0; raisedIssuesRules.clear(); quickFixesApplied.clear(); quickFixCountByRuleKey.clear(); helpAndFeedbackLinkClickedCount.clear(); analysisReportingCountersByType.clear(); findingsFilteredCountersByType.clear(); fixSuggestionReceivedCounter.clear(); fixSuggestionResolved.clear(); issuesUuidAiFixableSeen.clear(); codeFocusChangedCount = 0; manualAddedBindingsCount = 0; importedAddedBindingsCount = 0; autoAddedBindingsCount = 0; exportedConnectedModeCount = 0; newBindingsPropertiesFileCount = 0; newBindingsRemoteUrlCount = 0; newBindingsProjectNameCount = 0; newBindingsSharedConfigurationCount = 0; suggestedRemoteBindingsCount = 0; newIssuesFoundCount = 0; issuesFixedCount = 0; biggestNumberOfFilesInConfigScope = 0; calledToolsByName.clear(); dependencyRiskInvestigatedLocallyCount = 0; dependencyRiskInvestigatedRemotelyCount = 0; automaticAnalysisToggledCount = 0; mcpServerConfigurationRequestedCount = 0; mcpRuleFileRequestedCount = 0; isMcpIntegrationEnabled = false; mcpTransportModeUsed = null; labsLinkClickedCount.clear(); labsFeedbackLinkClickedCount.clear(); aiHooksInstalledCount.clear(); campaignsShown.clear(); campaignsResolutions.clear(); supportedLanguagesPanelOpenedCount = 0; supportedLanguagesPanelCtaClickedCount = 0; } public long numUseDays() { return numUseDays; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean enabled() { return enabled; } /** * Register that an analysis was performed. * This should be used when multiple files are analyzed. * * @see #setUsedAnalysis(String, int) */ void setUsedAnalysis() { markSonarLintAsUsedToday(); } private void markSonarLintAsUsedToday() { var now = LocalDate.now(); if (lastUseDate == null || !lastUseDate.equals(now)) { numUseDays++; } lastUseDate = now; } /** * Register the analysis of a single file, with information regarding language and duration of the analysis. */ void setUsedAnalysis(String language, int analysisTimeMs) { markSonarLintAsUsedToday(); var analyzer = analyzers.computeIfAbsent(language, x -> new TelemetryAnalyzerPerformance()); analyzer.registerAnalysis(analysisTimeMs); } static boolean isOlder(@Nullable LocalDate first, @Nullable LocalDate second) { return first == null || (second != null && first.isBefore(second)); } static boolean isOlder(@Nullable LocalDateTime first, @Nullable LocalDateTime second) { return first == null || (second != null && first.isBefore(second)); } @Override public void validateAndMigrate() { var today = LocalDate.now(); // migrate deprecated installDate if (installDate != null && (installTime == null || installTime.toLocalDate().isAfter(installDate))) { setInstallTime(installDate.atTime(OffsetTime.now())); } // fix install time if necessary if (installTime == null || installTime.isAfter(OffsetDateTime.now())) { setInstallTime(OffsetDateTime.now()); } // calculate use days if (lastUseDate == null) { numUseDays = 0; analyzers.clear(); return; } if (lastUseDate.isBefore(installTime.toLocalDate())) { lastUseDate = installTime.toLocalDate(); } else if (lastUseDate.isAfter(today)) { lastUseDate = today; } var maxUseDays = installTime.toLocalDate().until(lastUseDate, DAYS) + 1; if (numUseDays() > maxUseDays) { numUseDays = maxUseDays; } } public void incrementDevNotificationsCount(String eventType) { this.notificationsCountersByEventType.computeIfAbsent(eventType, k -> new TelemetryNotificationsCounter()).incrementDevNotificationsCount(); } public void incrementDevNotificationsClicked(String eventType) { markSonarLintAsUsedToday(); this.notificationsCountersByEventType.computeIfAbsent(eventType, k -> new TelemetryNotificationsCounter()).incrementDevNotificationsClicked(); } public void incrementShowHotspotRequestCount() { markSonarLintAsUsedToday(); showHotspotRequestsCount++; } public int showHotspotRequestsCount() { return showHotspotRequestsCount; } public void incrementShowIssueRequestCount() { markSonarLintAsUsedToday(); showIssueRequestsCount++; } public void fixSuggestionReceived(String suggestionId, AiSuggestionSource aiSuggestionSource, int snippetsCount, boolean wasGeneratedFromIde) { markSonarLintAsUsedToday(); this.fixSuggestionReceivedCounter.computeIfAbsent(suggestionId, k -> new TelemetryFixSuggestionReceivedCounter(aiSuggestionSource, snippetsCount, wasGeneratedFromIde)); } public void fixSuggestionResolved(String suggestionId, FixSuggestionStatus status, @Nullable Integer snippetIndex) { markSonarLintAsUsedToday(); var fixSuggestionSnippets = this.fixSuggestionResolved.computeIfAbsent(suggestionId, k -> new ArrayList<>()); var existingSnippetStatus = fixSuggestionSnippets.stream() .filter(s -> { var previousIndex = s.getFixSuggestionResolvedSnippetIndex(); return (snippetIndex == null && previousIndex == null) || (previousIndex != null && previousIndex.equals(snippetIndex)); }) .findFirst(); // if we already had a status for this snippet, we should replace it existingSnippetStatus.ifPresentOrElse(telemetryFixSuggestionResolvedStatus -> telemetryFixSuggestionResolvedStatus.setFixSuggestionResolvedStatus(status), () -> fixSuggestionSnippets.add(new TelemetryFixSuggestionResolvedStatus(status, snippetIndex))); } public void addIssuesWithPossibleAiFixFromIde(Set issues) { markSonarLintAsUsedToday(); issuesUuidAiFixableSeen.addAll(issues); } public int getShowIssueRequestsCount() { return showIssueRequestsCount; } public void incrementOpenHotspotInBrowserCount() { markSonarLintAsUsedToday(); openHotspotInBrowserCount++; } public int openHotspotInBrowserCount() { return openHotspotInBrowserCount; } public void incrementTaintVulnerabilitiesInvestigatedLocallyCount() { markSonarLintAsUsedToday(); taintVulnerabilitiesInvestigatedLocallyCount++; } public int taintVulnerabilitiesInvestigatedLocallyCount() { return taintVulnerabilitiesInvestigatedLocallyCount; } public void incrementTaintVulnerabilitiesInvestigatedRemotelyCount() { markSonarLintAsUsedToday(); taintVulnerabilitiesInvestigatedRemotelyCount++; } public int taintVulnerabilitiesInvestigatedRemotelyCount() { return taintVulnerabilitiesInvestigatedRemotelyCount; } public void helpAndFeedbackLinkClicked(String itemId) { this.helpAndFeedbackLinkClickedCount.computeIfAbsent(itemId, k -> new TelemetryHelpAndFeedbackCounter()).incrementHelpAndFeedbackLinkClickedCount(); } public void analysisReportingTriggered(AnalysisReportingType analysisType) { this.analysisReportingCountersByType.computeIfAbsent(analysisType, k -> new TelemetryAnalysisReportingCounter()).incrementAnalysisReportingCount(); } public void findingsFiltered(String filterType) { markSonarLintAsUsedToday(); this.findingsFilteredCountersByType.computeIfAbsent(filterType, k -> new TelemetryFindingsFilteredCounter()).incrementFindingsFilteredCount(); } public void incrementHotspotStatusChangedCount() { markSonarLintAsUsedToday(); hotspotStatusChangedCount++; } public int hotspotStatusChangedCount() { return hotspotStatusChangedCount; } public void addIssueStatusChanged(String ruleKey) { markSonarLintAsUsedToday(); issueStatusChangedRuleKeys.add(ruleKey); issueStatusChangedCount++; } public Set issueStatusChangedRuleKeys() { return issueStatusChangedRuleKeys; } public int issueStatusChangedCount() { return issueStatusChangedCount; } public void setInitialNewCodeFocus(boolean focusOnNewCode) { markSonarLintAsUsedToday(); this.isFocusOnNewCode = focusOnNewCode; } public void incrementNewCodeFocusChange() { markSonarLintAsUsedToday(); this.isFocusOnNewCode = !this.isFocusOnNewCode; codeFocusChangedCount++; } public void incrementManualAddedBindingsCount() { markSonarLintAsUsedToday(); manualAddedBindingsCount++; } public int getManualAddedBindingsCount() { return manualAddedBindingsCount; } public void incrementImportedAddedBindingsCount() { markSonarLintAsUsedToday(); importedAddedBindingsCount++; } public int getImportedAddedBindingsCount() { return importedAddedBindingsCount; } public void incrementAutoAddedBindingsCount() { markSonarLintAsUsedToday(); autoAddedBindingsCount++; } public int getAutoAddedBindingsCount() { return autoAddedBindingsCount; } public void incrementExportedConnectedModeCount() { markSonarLintAsUsedToday(); exportedConnectedModeCount++; } public void incrementNewBindingsPropertiesFileCount() { markSonarLintAsUsedToday(); newBindingsPropertiesFileCount++; } public void incrementNewBindingsRemoteUrlCount() { markSonarLintAsUsedToday(); newBindingsRemoteUrlCount++; } public void incrementNewBindingsProjectNameCount() { markSonarLintAsUsedToday(); newBindingsProjectNameCount++; } public void incrementNewBindingsSharedConfigurationCount() { markSonarLintAsUsedToday(); newBindingsSharedConfigurationCount++; } public void incrementSuggestedRemoteBindingsCount() { suggestedRemoteBindingsCount++; } public int getExportedConnectedModeCount() { return exportedConnectedModeCount; } public int getNewBindingsPropertiesFileCount() { return newBindingsPropertiesFileCount; } public int getNewBindingsRemoteUrlCount() { return newBindingsRemoteUrlCount; } public int getNewBindingsProjectNameCount() { return newBindingsProjectNameCount; } public int getNewBindingsSharedConfigurationCount() { return newBindingsSharedConfigurationCount; } public int getSuggestedRemoteBindingsCount() { return suggestedRemoteBindingsCount; } public void addNewlyFoundIssues(long newIssues) { markSonarLintAsUsedToday(); newIssuesFoundCount += newIssues; } public long getNewIssuesFoundCount() { return newIssuesFoundCount; } public void addFixedIssues(long fixedIssues) { markSonarLintAsUsedToday(); issuesFixedCount += fixedIssues; } public long getIssuesFixedCount() { return issuesFixedCount; } public void setMcpIntegrationEnabled(boolean isMcpIntegrationEnabled) { this.isMcpIntegrationEnabled = isMcpIntegrationEnabled; } public boolean isMcpIntegrationEnabled() { return isMcpIntegrationEnabled; } public void setMcpTransportModeUsed(McpTransportMode mcpTransportMode) { this.mcpTransportModeUsed = mcpTransportMode; } @CheckForNull public McpTransportMode getMcpTransportModeUsed() { return mcpTransportModeUsed; } public void incrementToolCalledCount(String toolName, boolean succeeded) { markSonarLintAsUsedToday(); calledToolsByName.computeIfAbsent(toolName, k -> new ToolCallCounter()).incrementCount(succeeded); } public Map getCalledToolsByName() { return calledToolsByName; } public void updateListFilesPerformance(int size, long timeMs) { if (size > biggestNumberOfFilesInConfigScope) { biggestNumberOfFilesInConfigScope = size; listingTimeForBiggestNumberConfigScopeFiles = timeMs; } if (timeMs > longestListingTimeForConfigScopeFiles) { longestListingTimeForConfigScopeFiles = timeMs; numberOfFilesForLongestFilesListingTimeConfigScope = size; } } public int getBiggestNumberOfFilesInConfigScope() { return biggestNumberOfFilesInConfigScope; } public long getListingTimeForBiggestNumberConfigScopeFiles() { return listingTimeForBiggestNumberConfigScopeFiles; } public int getNumberOfFilesForLongestFilesListingTimeConfigScope() { return numberOfFilesForLongestFilesListingTimeConfigScope; } public long getLongestListingTimeForConfigScopeFiles() { return longestListingTimeForConfigScopeFiles; } public void incrementHotspotInvestigatedLocallyCount() { markSonarLintAsUsedToday(); hotspotInvestigatedLocallyCount++; } public void incrementHotspotInvestigatedRemotelyCount() { markSonarLintAsUsedToday(); hotspotInvestigatedRemotelyCount++; } public void incrementTaintInvestigatedLocallyCount() { markSonarLintAsUsedToday(); taintInvestigatedLocallyCount++; } public void incrementTaintInvestigatedRemotelyCount() { markSonarLintAsUsedToday(); taintInvestigatedRemotelyCount++; } public void incrementIssueInvestigatedLocallyCount() { markSonarLintAsUsedToday(); issueInvestigatedLocallyCount++; } public void incrementDependencyRiskInvestigatedRemotelyCount() { markSonarLintAsUsedToday(); dependencyRiskInvestigatedRemotelyCount++; } public void incrementDependencyRiskInvestigatedLocallyCount() { markSonarLintAsUsedToday(); dependencyRiskInvestigatedLocallyCount++; } public int getHotspotInvestigatedRemotelyCount() { return hotspotInvestigatedRemotelyCount; } public int getHotspotInvestigatedLocallyCount() { return hotspotInvestigatedLocallyCount; } public int getTaintInvestigatedRemotelyCount() { return taintInvestigatedRemotelyCount; } public int getTaintInvestigatedLocallyCount() { return taintInvestigatedLocallyCount; } public int getIssueInvestigatedLocallyCount() { return issueInvestigatedLocallyCount; } public int getDependencyRiskInvestigatedRemotelyCount() { return dependencyRiskInvestigatedRemotelyCount; } public int getDependencyRiskInvestigatedLocallyCount() { return dependencyRiskInvestigatedLocallyCount; } public boolean isAutomaticAnalysisEnabled() { return isAutomaticAnalysisEnabled; } public int getAutomaticAnalysisToggledCount() { return automaticAnalysisToggledCount; } public void setInitialAutomaticAnalysisEnablement(boolean automaticAnalysisEnabled) { markSonarLintAsUsedToday(); this.isAutomaticAnalysisEnabled = automaticAnalysisEnabled; } public void incrementAutomaticAnalysisToggledCount() { markSonarLintAsUsedToday(); this.isAutomaticAnalysisEnabled = !this.isAutomaticAnalysisEnabled; automaticAnalysisToggledCount++; } public void incrementMcpServerConfigurationRequestedCount() { markSonarLintAsUsedToday(); mcpServerConfigurationRequestedCount++; } public int getMcpServerConfigurationRequestedCount() { return mcpServerConfigurationRequestedCount; } public void incrementMcpRuleFileRequestedCount() { markSonarLintAsUsedToday(); mcpRuleFileRequestedCount++; } public int getMcpRuleFileRequestedCount() { return mcpRuleFileRequestedCount; } public Map getLabsFeedbackLinkClickedCount() { return labsFeedbackLinkClickedCount; } public Map getLabsLinkClickedCount() { return labsLinkClickedCount; } public void ideLabsLinkClicked(String linkId) { this.labsLinkClickedCount.merge(linkId, 1, Integer::sum); } public void ideLabsFeedbackLinkClicked(String featureId) { this.labsFeedbackLinkClickedCount.merge(featureId, 1, Integer::sum); } public void aiHookInstalled(AiAgent aiAgent) { markSonarLintAsUsedToday(); this.aiHooksInstalledCount.merge(aiAgent, 1, Integer::sum); } public Map getAiHooksInstalledCount() { return aiHooksInstalledCount; } public void campaignShown(String campaignName) { campaignsShown.merge(campaignName, 1, Integer::sum); } public void campaignResolved(String campaignName, String campaignResolution) { campaignsResolutions.put(campaignName, campaignResolution); } public Map getCampaignsShown() { return campaignsShown; } public Map getCampaignsResolutions() { return campaignsResolutions; } public void incrementSupportedLanguagesPanelOpenedCount() { markSonarLintAsUsedToday(); supportedLanguagesPanelOpenedCount++; } public int getSupportedLanguagesPanelOpenedCount() { return supportedLanguagesPanelOpenedCount; } public void incrementSupportedLanguagesPanelCtaClickedCount() { markSonarLintAsUsedToday(); supportedLanguagesPanelCtaClickedCount++; } public int getSupportedLanguagesPanelCtaClickedCount() { return supportedLanguagesPanelCtaClickedCount; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorageManager.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import com.google.common.annotations.VisibleForTesting; import java.nio.file.Path; import java.time.Duration; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.storage.local.FileStorageManager; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryMigrationDto; import org.springframework.beans.factory.annotation.Qualifier; /** * Serialize and deserialize telemetry data to persistent storage. */ public class TelemetryLocalStorageManager { private final FileStorageManager fileStorageManager; @Nullable private final TelemetryMigrationDto telemetryMigration; public TelemetryLocalStorageManager(@Qualifier("telemetryPath") Path telemetryPath, InitializeParams initializeParams) { fileStorageManager = new FileStorageManager<>(telemetryPath, TelemetryLocalStorage::new, TelemetryLocalStorage.class); this.telemetryMigration = initializeParams.getTelemetryMigration(); } @VisibleForTesting TelemetryLocalStorage tryRead() { return getStorage(); } private TelemetryLocalStorage getStorage() { var inMemoryStorage = fileStorageManager.getStorage(); applyTelemetryMigration(inMemoryStorage); return inMemoryStorage; } private void applyTelemetryMigration(TelemetryLocalStorage inMemoryStorage) { if (needToMigrateTelemetry(inMemoryStorage)) { inMemoryStorage.setEnabled(telemetryMigration.isEnabled()); inMemoryStorage.setInstallTime(telemetryMigration.getInstallTime()); inMemoryStorage.setNumUseDays(telemetryMigration.getNumUseDays()); } } private boolean needToMigrateTelemetry(TelemetryLocalStorage inMemoryStorage) { if (telemetryMigration == null) { return false; } var duration = Duration.between(inMemoryStorage.installTime(), OffsetDateTime.now()); return duration.getSeconds() < 10 && inMemoryStorage.numUseDays() == 0; } public void tryUpdateAtomically(Consumer updater) { fileStorageManager.tryUpdateAtomically(updater); } public LocalDateTime lastUploadTime() { return getStorage().lastUploadTime(); } public boolean isEnabled() { return getStorage().enabled(); } public OffsetDateTime installTime() { return getStorage().installTime(); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManager.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.function.Consumer; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.telemetry.common.TelemetryUserSetting; /** * Manage telemetry data and persistent storage, and stateful telemetry actions. * The single central point for clients to manage telemetry. */ public class TelemetryManager implements TelemetryUserSetting { static final int MIN_HOURS_BETWEEN_UPLOAD = 5; private final TelemetryLocalStorageManager storageManager; private final TelemetryHttpClient client; TelemetryManager(TelemetryLocalStorageManager storageManager, TelemetryHttpClient client) { this.storageManager = storageManager; this.client = client; } void enable(TelemetryLiveAttributes telemetryLiveAttributes) { storageManager.tryUpdateAtomically(localStorage -> { localStorage.setEnabled(true); if (isGracePeriodElapsedAndDayChanged(localStorage.lastUploadTime())) { uploadAndClearTelemetry(telemetryLiveAttributes, localStorage); } }); } private static boolean isGracePeriodElapsedAndDayChanged(@Nullable LocalDateTime lastUploadTime) { return TelemetryUtils.isGracePeriodElapsedAndDayChanged(lastUploadTime, MIN_HOURS_BETWEEN_UPLOAD); } private void uploadAndClearTelemetry(TelemetryLiveAttributes telemetryLiveAttributes, TelemetryLocalStorage localStorage) { client.upload(localStorage, telemetryLiveAttributes); localStorage.setLastUploadTime(); localStorage.clearAfterPing(); } /** * Disable telemetry (opt-out). */ void disable(TelemetryLiveAttributes telemetryLiveAttributes) { storageManager.tryUpdateAtomically(data -> { data.setEnabled(false); client.optOut(data, telemetryLiveAttributes); }); } /** * Upload telemetry data, when all conditions are satisfied: * - telemetry is enabled * - the day is different from the last upload * - the grace period has elapsed since the last upload * To be called periodically once a day. */ void uploadAndClearTelemetry(TelemetryLiveAttributes telemetryLiveAttributes) { if (isTelemetryEnabledByUser() && isGracePeriodElapsedAndDayChanged(storageManager.lastUploadTime())) { storageManager.tryUpdateAtomically(localStorage -> uploadAndClearTelemetry(telemetryLiveAttributes, localStorage)); } } public void updateTelemetry(Consumer updater) { if (isTelemetryEnabledByUser()) { storageManager.tryUpdateAtomically(updater); } } @Override public boolean isTelemetryEnabledByUser() { return storageManager.isEnabled(); } public OffsetDateTime installTime() { return storageManager.installTime(); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryNotificationsCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; public class TelemetryNotificationsCounter { private int devNotificationsCount; private int devNotificationsClicked; public TelemetryNotificationsCounter() { } public TelemetryNotificationsCounter(int devNotificationsCount, int devNotificationsClicked) { this.devNotificationsCount = devNotificationsCount; this.devNotificationsClicked = devNotificationsClicked; } public int getDevNotificationsClicked() { return devNotificationsClicked; } public int getDevNotificationsCount() { return devNotificationsCount; } public void incrementDevNotificationsCount() { this.devNotificationsCount++; } public void incrementDevNotificationsClicked() { this.devNotificationsClicked++; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryServerAttributes.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.util.List; import javax.annotation.Nullable; /** * @param usesConnectedMode At least one project in the IDE is bound to a SQ server or SC * @param usesSonarCloud At least one project in the IDE is bound to SC * @param childBindingCount Number of bindings for a child configuration scope * @param sonarQubeServerBindingCount Number of bindings with SonarQube Server * @param sonarQubeCloudEUBindingCount Number of bindings with SonarQube Cloud EU * @param sonarQubeCloudUSBindingCount Number of bindings with SonarQube Cloud US * @param devNotificationsDisabled Are dev notifications disabled (if multiple connections are configured, return true if feature is disabled for at least one connection) * @param nonDefaultEnabledRules Rule keys for rules that disabled by default, but was enabled by user in settings. * @param defaultDisabledRules Rule keys for rules that enabled by default, but was disabled by user in settings. * @param nodeVersion Node.js version used by analyzers (detected or configured by the user). * Empty if no node present/detected/configured * @param connectionsAttributes Information about the connections configured in the IDE */ public record TelemetryServerAttributes(boolean usesConnectedMode, boolean usesSonarCloud, int childBindingCount, int sonarQubeServerBindingCount, int sonarQubeCloudEUBindingCount, int sonarQubeCloudUSBindingCount, boolean devNotificationsDisabled, List nonDefaultEnabledRules, List defaultDisabledRules, @Nullable String nodeVersion, List connectionsAttributes) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtils.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BinaryOperator; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryAnalyzerPerformancePayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryFixSuggestionPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryFixSuggestionResolvedPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryNotificationsCounterPayload; import org.sonarsource.sonarlint.core.telemetry.payload.TelemetryNotificationsPayload; class TelemetryUtils { private TelemetryUtils() { // utility class, forbidden constructor } /** * Check if "now" is a different day than the reference. * * @param date reference date * @return true if it's a different day than the reference */ static boolean isGracePeriodElapsedAndDayChanged(@Nullable LocalDate date) { return date == null || !date.equals(LocalDate.now()); } /** * Transforms stored information about analyzers performance to payload to be sent to server. */ static TelemetryAnalyzerPerformancePayload[] toPayload(Map analyzers) { return analyzers.entrySet().stream() .map(TelemetryUtils::toPayload) .toArray(TelemetryAnalyzerPerformancePayload[]::new); } private static TelemetryAnalyzerPerformancePayload toPayload(Map.Entry entry) { var analyzerPerformance = entry.getValue(); var language = entry.getKey(); var analysisCount = analyzerPerformance.analysisCount(); Map distribution = analyzerPerformance .frequencies().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> { if (analysisCount == 0) { return BigDecimal.ZERO.setScale(2); } return BigDecimal.valueOf(100) .multiply(BigDecimal.valueOf(e.getValue())) .divide(BigDecimal.valueOf(analysisCount), 2, RoundingMode.HALF_EVEN); }, throwingMerger(), LinkedHashMap::new)); return new TelemetryAnalyzerPerformancePayload(language, distribution); } static TelemetryNotificationsPayload toPayload(boolean devNotificationsDisabled, Map notifications) { return new TelemetryNotificationsPayload(devNotificationsDisabled, toNotifPayload(notifications)); } private static Map toNotifPayload(Map notifications) { return notifications.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new TelemetryNotificationsCounterPayload(e.getValue().getDevNotificationsCount(), e.getValue().getDevNotificationsClicked()))); } static TelemetryFixSuggestionPayload[] toFixSuggestionResolvedPayload( Map fixSuggestionReceivedCounter, Map> fixSuggestionResolved ) { return fixSuggestionReceivedCounter.entrySet().stream().map(e -> { var suggestionId = e.getKey(); var snippetsCount = e.getValue().snippetsCount(); var source = e.getValue().aiSuggestionsSource(); var resolvedSnippetStatus = fixSuggestionResolved.getOrDefault(suggestionId, List.of(new TelemetryFixSuggestionResolvedStatus(null, null))); var resolvedSnippetPayload = resolvedSnippetStatus.stream() .map(s -> new TelemetryFixSuggestionResolvedPayload(s.getFixSuggestionResolvedStatus(), s.getFixSuggestionResolvedSnippetIndex())).toList(); var wasGeneratedFromIde = e.getValue().wasGeneratedFromIde(); return new TelemetryFixSuggestionPayload(suggestionId, snippetsCount, source, resolvedSnippetPayload, wasGeneratedFromIde); }).toArray(TelemetryFixSuggestionPayload[]::new); } /** * Check if "now" is a different day than the reference, and some hours have elapsed. * * @param dateTime reference date * @param hours minimum hours that must have elapsed * @return true if it's a different day than the reference and at least hours have elapsed */ static boolean isGracePeriodElapsedAndDayChanged(@Nullable LocalDateTime dateTime, long hours) { return dateTime == null || (!LocalDate.now().equals(dateTime.toLocalDate()) && (dateTime.until(LocalDateTime.now(), ChronoUnit.HOURS) >= hours)); } private static BinaryOperator throwingMerger() { return (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/ToolCallCounter.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; public class ToolCallCounter { private int success; private int error; public ToolCallCounter() { } public ToolCallCounter(int success, int error) { this.success = success; this.error = error; } public void incrementCount(boolean succeeded) { if (succeeded) { success++; } else { error++; } } public int getSuccess() { return success; } public int getError() { return error; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/common/TelemetryUserSetting.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.common; public interface TelemetryUserSetting { boolean isTelemetryEnabledByUser(); } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/common/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.common; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/GessieHttpClient.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie; import com.google.common.annotations.VisibleForTesting; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger; import org.sonarsource.sonarlint.core.http.HttpClient; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.telemetry.InternalDebug; import org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieEvent; import org.springframework.beans.factory.annotation.Qualifier; public class GessieHttpClient { private static final SonarLintLogger LOG = SonarLintLogger.get(); private final Gson gson = configureGson(); private final HttpClient client; private final String endpoint; public GessieHttpClient(HttpClientProvider httpClientProvider, @Qualifier("gessieEndpoint") String gessieEndpoint, @Qualifier("gessieApiKey") String gessieApiKey) { this.client = httpClientProvider.getHttpClientWithXApiKeyAndRetries(gessieApiKey); this.endpoint = gessieEndpoint; } public void postEvent(GessieEvent event) { var json = gson.toJson(event); logGessiePayload(json); var futureResponse = client.postAsync(endpoint + "/ide", HttpClient.JSON_CONTENT_TYPE, json); handleGessieResponse(futureResponse); } private void logGessiePayload(String json) { if (isTelemetryLogEnabled()) { LOG.info("Sending Gessie payload.\n{}", json); } } private static Gson configureGson() { return new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .serializeNulls() .create(); } private static void handleGessieResponse(CompletableFuture responseCompletableFuture) { responseCompletableFuture.thenAccept(response -> { if (!response.isSuccessful() && InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry to Gessie: {} \n{}", response, response.bodyAsString()); } }).exceptionally(exception -> { if (InternalDebug.isEnabled()) { LOG.error("Failed to upload telemetry to Gessie", exception); } return null; }); } @VisibleForTesting boolean isTelemetryLogEnabled(){ return Boolean.parseBoolean(System.getenv("SONARLINT_TELEMETRY_LOG")); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/GessieService.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie; import jakarta.annotation.PostConstruct; import java.time.Instant; import java.util.UUID; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto; import org.sonarsource.sonarlint.core.telemetry.common.TelemetryUserSetting; import org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieEvent; import org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata; import org.sonarsource.sonarlint.core.telemetry.gessie.event.payload.MessagePayload; import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.GESSIE_TELEMETRY; import static org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata.SonarLintDomain; public class GessieService { private final boolean isGessieFeatureEnabled; private final TelemetryClientConstantAttributesDto telemetryConstantAttributes; private final GessieHttpClient client; private final TelemetryUserSetting userSetting; public GessieService(InitializeParams initializeParams, GessieHttpClient client, TelemetryUserSetting userSetting) { this.isGessieFeatureEnabled = initializeParams.getBackendCapabilities().contains(GESSIE_TELEMETRY); this.telemetryConstantAttributes = initializeParams.getTelemetryConstantAttributes(); this.client = client; this.userSetting = userSetting; } @PostConstruct public void onStartup() { if (isGessieFeatureEnabled && userSetting.isTelemetryEnabledByUser()) { client.postEvent(new GessieEvent( new GessieMetadata(UUID.randomUUID(), new GessieMetadata.GessieSource(SonarLintDomain.fromProductKey(telemetryConstantAttributes.getProductKey())), "Analytics.Editor.PluginActivated", Long.toString(Instant.now().toEpochMilli()), "0"), new MessagePayload("Gessie integration test event", "slcore_start") )); } } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/GessieEvent.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie.event; public record GessieEvent( GessieMetadata metadata, Object eventPayload ) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/GessieMetadata.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie.event; import com.google.gson.annotations.SerializedName; import java.util.UUID; public record GessieMetadata( UUID eventId, GessieSource source, String eventType, String eventTimestamp, String eventVersion ) { public record GessieSource(SonarLintDomain domain) { } public enum SonarLintDomain { @SerializedName("VSCode") VS_CODE, @SerializedName("VisualStudio") VISUAL_STUDIO, @SerializedName("Eclipse") ECLIPSE, @SerializedName("IntelliJ") INTELLIJ, @SerializedName("SLCore") SLCORE; public static SonarLintDomain fromProductKey(String productKey) { return switch (productKey) { case "idea" -> INTELLIJ; case "eclipse" -> ECLIPSE; case "visualstudio" -> VISUAL_STUDIO; case "vscode", "cursor", "windsurf" -> VS_CODE; default -> SLCORE; }; } } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.gessie.event; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/payload/MessagePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie.event.payload; public record MessagePayload( String message, String trigger ) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/payload/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.gessie.event.payload; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/gessie/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.gessie; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresBuilder.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.Gson; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import org.sonarsource.sonarlint.core.telemetry.TelemetryLiveAttributes; import org.sonarsource.sonarlint.core.telemetry.TelemetryLocalStorage; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueGranularity.DAILY; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.BOOLEAN; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.INTEGER; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.STRING; public class TelemetryMeasuresBuilder { private static final String LINK_CLICKED_BASE_NAME = "link_clicked_count_"; private static final String FEEDBACK_CLICKED_BASE_NAME = "feedback_link_clicked_count_"; private final String platform; private final String product; private final TelemetryLocalStorage storage; private final TelemetryLiveAttributes liveAttributes; public TelemetryMeasuresBuilder(String platform, String product, TelemetryLocalStorage storage, TelemetryLiveAttributes liveAttributes) { this.platform = platform; this.product = product; this.storage = storage; this.liveAttributes = liveAttributes; } public TelemetryMeasuresPayload build() { var values = new ArrayList(); addConnectedModeMeasures(values); addNewBindingsMeasures(values); addBindingSuggestionClueMeasures(values); addHelpAndFeedbackMeasures(values); addAnalysisReportingMeasures(values); addQuickFixMeasures(values); addIssuesMeasures(values); addToolsMeasures(values); addFindingsFilteredMeasures(values); addPerformanceMeasures(values); addFindingInvestigationMeasures(values); addAutomaticAnalysisMeasures(values); addMCPMeasures(values); addLabsMeasures(values); addAiHooksMeasures(values); addCampaignsMeasures(values); addSupportedLanguagesPanelMeasures(values); return new TelemetryMeasuresPayload(UUID.randomUUID().toString(), platform, storage.installTime(), product, TelemetryMeasuresDimension.INSTALLATION, values); } private void addPerformanceMeasures(ArrayList values) { values.add(new TelemetryMeasuresValue("performance.largest_file_count", String.valueOf(storage.getBiggestNumberOfFilesInConfigScope()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("performance.largest_file_count_ms", String.valueOf(storage.getListingTimeForBiggestNumberConfigScopeFiles()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("performance.longest_file_count_ms", String.valueOf(storage.getLongestListingTimeForConfigScopeFiles()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("performance.longest_file_count", String.valueOf(storage.getNumberOfFilesForLongestFilesListingTimeConfigScope()), INTEGER, DAILY)); } private void addFindingInvestigationMeasures(ArrayList values) { values.add(new TelemetryMeasuresValue("findings_investigation.taints_locally", String.valueOf(storage.getTaintInvestigatedLocallyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.taints_remotely", String.valueOf(storage.getTaintInvestigatedRemotelyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.hotspots_locally", String.valueOf(storage.getHotspotInvestigatedLocallyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.hotspots_remotely", String.valueOf(storage.getHotspotInvestigatedRemotelyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.issues_locally", String.valueOf(storage.getIssueInvestigatedLocallyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.dependency_risks_locally", String.valueOf(storage.getDependencyRiskInvestigatedLocallyCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("findings_investigation.dependency_risks_remotely", String.valueOf(storage.getDependencyRiskInvestigatedRemotelyCount()), INTEGER, DAILY)); } private void addConnectedModeMeasures(ArrayList values) { if (liveAttributes.usesConnectedMode()) { values.add(new TelemetryMeasuresValue("shared_connected_mode.manual", String.valueOf(storage.getManualAddedBindingsCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.imported", String.valueOf(storage.getImportedAddedBindingsCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.auto", String.valueOf(storage.getAutoAddedBindingsCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.exported", String.valueOf(storage.getExportedConnectedModeCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.child_count", String.valueOf(liveAttributes.countChildBindings()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.server_count", String.valueOf(liveAttributes.countSonarQubeServerBindings()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.cloud_eu_count", String.valueOf(liveAttributes.countSonarQubeCloudEUBindings()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.cloud_us_count", String.valueOf(liveAttributes.countSonarQubeCloudUSBindings()), INTEGER, DAILY)); if (!liveAttributes.getConnectionsAttributes().isEmpty()) { values.add(new TelemetryMeasuresValue("connections.attributes", new Gson().toJson(liveAttributes.getConnectionsAttributes()), STRING, DAILY)); } } } private void addNewBindingsMeasures(ArrayList values) { if (liveAttributes.usesConnectedMode()) { values.add(new TelemetryMeasuresValue("new_bindings.manual", String.valueOf(storage.getManualAddedBindingsCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_remote_url", String.valueOf(storage.getNewBindingsRemoteUrlCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_properties_file", String.valueOf(storage.getNewBindingsPropertiesFileCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_shared_config_file", String.valueOf(storage.getNewBindingsSharedConfigurationCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_project_name", String.valueOf(storage.getNewBindingsProjectNameCount()), INTEGER, DAILY)); } } private void addBindingSuggestionClueMeasures(ArrayList values) { values.add(new TelemetryMeasuresValue("binding_suggestion_clue.remote_url", String.valueOf(storage.getSuggestedRemoteBindingsCount()), INTEGER, DAILY)); } private void addHelpAndFeedbackMeasures(List values) { storage.getHelpAndFeedbackLinkClickedCounter().entrySet().stream() .filter(e -> e.getValue().getHelpAndFeedbackLinkClickedCount() > 0) .map(e -> new TelemetryMeasuresValue( "help_and_feedback." + e.getKey().toLowerCase(Locale.ROOT), String.valueOf(e.getValue().getHelpAndFeedbackLinkClickedCount()), INTEGER, DAILY )) .forEach(values::add); } private void addAnalysisReportingMeasures(List values) { storage.getAnalysisReportingCountersByType().entrySet().stream() .filter(e -> e.getValue().getAnalysisReportingCount() > 0) .map(e -> new TelemetryMeasuresValue( "analysis_reporting." + e.getKey().getId(), String.valueOf(e.getValue().getAnalysisReportingCount()), INTEGER, DAILY )) .forEach(values::add); } private void addQuickFixMeasures(List values) { var allQuickFixCount = storage.getQuickFixCountByRuleKey().values().stream() .mapToInt(Integer::intValue) .sum(); if (allQuickFixCount > 0) { values.add(new TelemetryMeasuresValue( "quick_fix.applied_count", Integer.toString(allQuickFixCount), INTEGER, DAILY )); } } private void addIssuesMeasures(List values) { var newIssuesFound = storage.getNewIssuesFoundCount(); if (newIssuesFound > 0) { values.add(new TelemetryMeasuresValue("ide_issues.found", Long.toString(newIssuesFound), INTEGER, DAILY)); } var issuesFixed = storage.getIssuesFixedCount(); if (issuesFixed > 0) { values.add(new TelemetryMeasuresValue("ide_issues.fixed", Long.toString(issuesFixed), INTEGER, DAILY)); } } private void addToolsMeasures(List values) { var calledToolsByName = storage.getCalledToolsByName(); calledToolsByName.forEach((key, toolCallCounter) -> { values.add(new TelemetryMeasuresValue("tools." + key + "_success_count", Integer.toString(toolCallCounter.getSuccess()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("tools." + key + "_error_count", Integer.toString(toolCallCounter.getError()), INTEGER, DAILY)); }); } private void addFindingsFilteredMeasures(List values) { storage.getFindingsFilteredCountersByType().entrySet().stream() .filter(e -> e.getValue().getFindingsFilteredCount() > 0) .map(e -> new TelemetryMeasuresValue( "findings_filtered." + e.getKey().toLowerCase(Locale.ROOT), String.valueOf(e.getValue().getFindingsFilteredCount()), INTEGER, DAILY )) .forEach(values::add); } private void addAutomaticAnalysisMeasures(List values) { values.add(new TelemetryMeasuresValue("automatic_analysis.enabled", String.valueOf(storage.isAutomaticAnalysisEnabled()), BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("automatic_analysis.toggled_count", String.valueOf(storage.getAutomaticAnalysisToggledCount()), INTEGER, DAILY)); } private void addMCPMeasures(List values) { values.add(new TelemetryMeasuresValue("mcp.configuration_requested", String.valueOf(storage.getMcpServerConfigurationRequestedCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("mcp.rule_file_requested", String.valueOf(storage.getMcpRuleFileRequestedCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("mcp.integration_enabled", Boolean.toString(storage.isMcpIntegrationEnabled()), BOOLEAN, DAILY)); var mcpTransportModeUsed = storage.getMcpTransportModeUsed(); if (mcpTransportModeUsed != null) { values.add(new TelemetryMeasuresValue("mcp.transport_mode", mcpTransportModeUsed.name(), STRING, DAILY)); } } private void addLabsMeasures(ArrayList values) { values.add(new TelemetryMeasuresValue("ide_labs.joined", String.valueOf(liveAttributes.hasJoinedIdeLabs()), BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.enabled", String.valueOf(liveAttributes.hasEnabledIdeLabs()), BOOLEAN, DAILY)); addAll(storage.getLabsLinkClickedCount(), LINK_CLICKED_BASE_NAME, values); addAll(storage.getLabsFeedbackLinkClickedCount(), FEEDBACK_CLICKED_BASE_NAME, values); } private static void addAll(Map clickCounts, String baseName, List values) { clickCounts.entrySet().stream() .filter(entry -> entry.getValue() > 0) .map(entry -> new TelemetryMeasuresValue( "ide_labs." + baseName + entry.getKey(), String.valueOf(entry.getValue()), INTEGER, DAILY)) .forEach(values::add); } private void addAiHooksMeasures(ArrayList values) { storage.getAiHooksInstalledCount().entrySet().stream() .filter(entry -> entry.getValue() > 0) .map(entry -> new TelemetryMeasuresValue( "ai_hooks." + entry.getKey().name().toLowerCase(Locale.ROOT) + "_installed", String.valueOf(entry.getValue()), INTEGER, DAILY)) .forEach(values::add); } private void addSupportedLanguagesPanelMeasures(List values) { values.add(new TelemetryMeasuresValue("supported_languages_panel.opened_count", String.valueOf(storage.getSupportedLanguagesPanelOpenedCount()), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("supported_languages_panel.cta_clicked_count", String.valueOf(storage.getSupportedLanguagesPanelCtaClickedCount()), INTEGER, DAILY)); } private void addCampaignsMeasures(ArrayList values) { storage.getCampaignsShown().entrySet().stream() .filter(entry -> entry.getValue() > 0) .map(campaignShown -> new TelemetryMeasuresValue( "campaigns." + campaignShown.getKey() + "_shown", String.valueOf(campaignShown.getValue()), INTEGER, DAILY)) .forEach(values::add); storage.getCampaignsResolutions().entrySet().stream() .map(campaignsResolution -> new TelemetryMeasuresValue( "campaigns." + campaignsResolution.getKey() + "_resolution", campaignsResolution.getValue(), STRING, DAILY)) .forEach(values::add); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresDimension.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.annotations.SerializedName; public enum TelemetryMeasuresDimension { @SerializedName("language") LANGUAGE, @SerializedName("installation") INSTALLATION } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; import java.time.OffsetDateTime; import java.util.List; import org.sonarsource.sonarlint.core.commons.storage.adapter.OffsetDateTimeAdapter; public record TelemetryMeasuresPayload(@SerializedName("message_uuid") String messageUuid, @SerializedName("os") String os, @SerializedName("install_time") OffsetDateTime installTime, @SerializedName("sonarlint_product") String product, @SerializedName("dimension") TelemetryMeasuresDimension dimension, @SerializedName("metric_values") List values) { public String toJson() { var gson = new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()) .serializeNulls() .create(); var jsonPayload = gson.toJsonTree(this).getAsJsonObject(); return gson.toJson(jsonPayload); } public boolean hasMetrics() { return !values.isEmpty(); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresValue.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.annotations.SerializedName; public class TelemetryMeasuresValue { private static final String KEY_PATTERN = "^([a-z_][a-z0-9_]{1,126}\\.)[a-z_][a-z0-9_]{1,126}$"; @SerializedName("key") private final String key; @SerializedName("value") private final String value; @SerializedName("type") private final TelemetryMeasuresValueType type; @SerializedName("granularity") private final TelemetryMeasuresValueGranularity granularity; public TelemetryMeasuresValue(String key, String value, TelemetryMeasuresValueType type, TelemetryMeasuresValueGranularity granularity) { this.key = validateKey(key); this.value = value; this.type = type; this.granularity = granularity; } /* * From the telemetry measures specification: * - Entire key: ^([a-z_][a-z0-9_]{1,126}\.)[a-z_][a-z0-9_]{1,126}$ * - Group name: ^[a-z_][a-z0-9_]{1,126}\.$ * - Measure name: ^[a-z_][a-z0-9_]{1,126}$ */ private static String validateKey(String maybeKey) throws IllegalArgumentException { if (maybeKey.matches(KEY_PATTERN)) { return maybeKey; } throw new IllegalArgumentException("Invalid measure key: " + maybeKey); } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresValueGranularity.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.annotations.SerializedName; public enum TelemetryMeasuresValueGranularity { @SerializedName("daily") DAILY, @SerializedName("weekly") WEEKLY, @SerializedName("monthly") MONTHLY } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/TelemetryMeasuresValueType.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.measures.payload; import com.google.gson.annotations.SerializedName; public enum TelemetryMeasuresValueType { @SerializedName("string") STRING, @SerializedName("integer") INTEGER, @SerializedName("boolean") BOOLEAN, @SerializedName("float") FLOAT } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/measures/payload/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.measures.payload; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/HotspotPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; public record HotspotPayload(@SerializedName("open_in_browser_count") int openInBrowserCount, @SerializedName("status_changed_count") int statusChangedCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/IssuePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.util.Set; public record IssuePayload(@SerializedName("status_changed_rule_keys") Set statusChangedRuleKeys, @SerializedName("status_changed_count") int statusChangedCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShareConnectedModePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import javax.annotation.Nullable; public record ShareConnectedModePayload(@SerializedName("manual_bindings_count") @Nullable Integer manualAddedBindingsCount, @SerializedName("imported_bindings_count") @Nullable Integer importedAddedBindingsCount, @SerializedName("auto_bindings_count") @Nullable Integer autoAddedBindingsCount, @SerializedName("exported_connected_mode_count") @Nullable Integer exportedConnectedModeCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowHotspotPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; public record ShowHotspotPayload(@SerializedName("requests_count") int requestsCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowIssuePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; public record ShowIssuePayload(@SerializedName("requests_count") int requestsCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TaintVulnerabilitiesPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; public record TaintVulnerabilitiesPayload(@SerializedName("investigated_locally_count") int investigatedLocallyCount, @SerializedName("investigated_remotely_count") int investigatedRemotelyCount) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryAnalyzerPerformancePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.math.BigDecimal; import java.util.Map; public record TelemetryAnalyzerPerformancePayload(String language, @SerializedName("rate_per_duration") Map distribution) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.util.List; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; public record TelemetryFixSuggestionPayload(@SerializedName("suggestion_id") String suggestionId, @SerializedName("count_snippets") int countSnippets, @SerializedName("ai_fix_suggestion_provider") AiSuggestionSource aiFixSuggestionProvider, @SerializedName("snippets") List snippets, @SerializedName("was_ai_fix_suggestion_generated_from_ide") boolean wasAiFixSuggestionGeneratedFromIde) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionResolvedPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; public record TelemetryFixSuggestionResolvedPayload(@SerializedName("status") @Nullable FixSuggestionStatus status, @SerializedName("snippet_index") @Nullable Integer snippetIndex) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryHelpAndFeedbackPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.util.HashMap; import java.util.Map; import org.sonarsource.sonarlint.core.telemetry.TelemetryHelpAndFeedbackCounter; public class TelemetryHelpAndFeedbackPayload { @SerializedName("count_by_link") private final Map counters; public TelemetryHelpAndFeedbackPayload(Map counters) { this.counters = new HashMap<>(); counters.forEach((link, count) -> this.counters.put(link, count.getHelpAndFeedbackLinkClickedCount())); } public Map getCounters() { return counters; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsCounterPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; public record TelemetryNotificationsCounterPayload(@SerializedName("received") int devNotificationsCount, @SerializedName("clicked") int devNotificationsClicked) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.util.Map; public record TelemetryNotificationsPayload(boolean disabled, @SerializedName("count_by_type") Map counters) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import java.time.OffsetDateTime; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.commons.storage.adapter.OffsetDateTimeAdapter; import org.sonarsource.sonarlint.core.telemetry.payload.cayc.CleanAsYouCodePayload; /** * Models the usage data uploaded */ public class TelemetryPayload { @SerializedName("days_since_installation") private final long daysSinceInstallation; @SerializedName("days_of_use") private final long daysOfUse; @SerializedName("sonarlint_version") private final String version; @SerializedName("sonarlint_product") private final String product; @SerializedName("ide_version") private final String ideVersion; @SerializedName("platform") private final String platform; @SerializedName("architecture") private final String architecture; @SerializedName("connected_mode_used") private final boolean connectedMode; @SerializedName("connected_mode_sonarcloud") private final boolean connectedModeSonarcloud; @SerializedName("system_time") private final OffsetDateTime systemTime; @SerializedName("install_time") private final OffsetDateTime installTime; @SerializedName("os") private final String os; @SerializedName("jre") private final String jre; @SerializedName("nodejs") private final String nodejs; @SerializedName("analyses") private final TelemetryAnalyzerPerformancePayload[] analyses; @SerializedName("server_notifications") private final TelemetryNotificationsPayload notifications; @SerializedName("show_hotspot") private final ShowHotspotPayload showHotspotPayload; @SerializedName("show_issue") private final ShowIssuePayload showIssuePayload; @SerializedName("taint_vulnerabilities") private final TaintVulnerabilitiesPayload taintVulnerabilitiesPayload; @SerializedName("rules") private final TelemetryRulesPayload telemetryRulesPayload; @SerializedName("hotspot") private final HotspotPayload hotspotPayload; @SerializedName("issue") private final IssuePayload issuePayload; @SerializedName("help_and_feedback") private final TelemetryHelpAndFeedbackPayload helpAndFeedbackPayload; @SerializedName("ai_fix_suggestions") private final TelemetryFixSuggestionPayload[] aiFixSuggestionsPayload; @SerializedName("count_issues_with_possible_ai_fix_from_ide") private final int countIssuesWithPossibleAiFixFromIde; @SerializedName("cayc") private final CleanAsYouCodePayload cleanAsYouCodePayload; @SerializedName("shared_connected_mode") private final ShareConnectedModePayload shareConnectedModePayload; private final transient Map additionalAttributes; public TelemetryPayload(long daysSinceInstallation, long daysOfUse, String product, String version, String ideVersion, @Nullable String platform, @Nullable String architecture, boolean connectedMode, boolean connectedModeSonarcloud, OffsetDateTime systemTime, OffsetDateTime installTime, String os, String jre, @Nullable String nodejs, TelemetryAnalyzerPerformancePayload[] analyses, TelemetryNotificationsPayload notifications, ShowHotspotPayload showHotspotPayload, ShowIssuePayload showIssuePayload, TaintVulnerabilitiesPayload taintVulnerabilitiesPayload, TelemetryRulesPayload telemetryRulesPayload, HotspotPayload hotspotPayload, IssuePayload issuePayload, TelemetryHelpAndFeedbackPayload helpAndFeedbackPayload, TelemetryFixSuggestionPayload[] aiFixSuggestionsPayload, int countIssuesWithPossibleAiFixFromIde, CleanAsYouCodePayload cleanAsYouCodePayload, ShareConnectedModePayload shareConnectedModePayload, Map additionalAttributes) { this.daysSinceInstallation = daysSinceInstallation; this.daysOfUse = daysOfUse; this.product = product; this.version = version; this.ideVersion = ideVersion; this.platform = platform; this.architecture = architecture; this.connectedMode = connectedMode; this.connectedModeSonarcloud = connectedModeSonarcloud; this.systemTime = systemTime; this.installTime = installTime; this.os = os; this.jre = jre; this.nodejs = nodejs; this.analyses = analyses; this.notifications = notifications; this.showHotspotPayload = showHotspotPayload; this.showIssuePayload = showIssuePayload; this.taintVulnerabilitiesPayload = taintVulnerabilitiesPayload; this.telemetryRulesPayload = telemetryRulesPayload; this.hotspotPayload = hotspotPayload; this.issuePayload = issuePayload; this.helpAndFeedbackPayload = helpAndFeedbackPayload; this.aiFixSuggestionsPayload = aiFixSuggestionsPayload; this.countIssuesWithPossibleAiFixFromIde = countIssuesWithPossibleAiFixFromIde; this.cleanAsYouCodePayload = cleanAsYouCodePayload; this.shareConnectedModePayload = shareConnectedModePayload; this.additionalAttributes = additionalAttributes; } public long daysSinceInstallation() { return daysSinceInstallation; } public long daysOfUse() { return daysOfUse; } public TelemetryAnalyzerPerformancePayload[] analyses() { return analyses; } public String version() { return version; } public String product() { return product; } public boolean connectedMode() { return connectedMode; } public boolean connectedModeSonarcloud() { return connectedModeSonarcloud; } public String os() { return os; } public String jre() { return jre; } public String nodejs() { return nodejs; } public OffsetDateTime systemTime() { return systemTime; } public TelemetryNotificationsPayload notifications() { return notifications; } public TelemetryHelpAndFeedbackPayload helpAndFeedbackPayload() { return helpAndFeedbackPayload; } public CleanAsYouCodePayload cleanAsYouCodePayload() { return cleanAsYouCodePayload; } public IssuePayload issuePayload() { return issuePayload; } public Map additionalAttributes() { return additionalAttributes; } public ShowHotspotPayload getShowHotspotPayload() { return showHotspotPayload; } public ShowIssuePayload getShowIssuePayload() { return showIssuePayload; } public TaintVulnerabilitiesPayload getTaintVulnerabilitiesPayload() { return taintVulnerabilitiesPayload; } public TelemetryRulesPayload getTelemetryRulesPayload() { return telemetryRulesPayload; } public HotspotPayload getHotspotPayload() { return hotspotPayload; } public ShareConnectedModePayload getShareConnectedModePayload() { return shareConnectedModePayload; } public TelemetryFixSuggestionPayload[] getAiFixSuggestionsPayload() { return aiFixSuggestionsPayload; } public String getIdeVersion() { return ideVersion; } public String getPlatform() { return platform; } public String getArchitecture() { return architecture; } public OffsetDateTime getInstallTime() { return installTime; } public int getCountIssuesWithPossibleAiFixFromIde() { return countIssuesWithPossibleAiFixFromIde; } public String toJson() { var gson = new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()) .serializeNulls() .create(); var jsonPayload = gson.toJsonTree(this).getAsJsonObject(); var jsonAdditional = gson.toJsonTree(additionalAttributes, new TypeToken>() { }.getType()).getAsJsonObject(); return gson.toJson(mergeObjects(jsonAdditional, jsonPayload)); } static JsonObject mergeObjects(JsonObject source, JsonObject target) { for (Entry entry : source.entrySet()) { var value = entry.getValue(); if (!target.has(entry.getKey())) { // new value for "key": target.add(entry.getKey(), value); } else if (value.isJsonObject()) { // existing value for "key" - recursively deep merge: var valueJson = (JsonObject) value; mergeObjects(valueJson, target.getAsJsonObject(entry.getKey())); } // Don't override value if it already exists in the target } return target; } } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryRulesPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.annotations.SerializedName; import java.util.Collection; public record TelemetryRulesPayload(@SerializedName("non_default_enabled") Collection nonDefaultEnabled, @SerializedName("default_disabled") Collection defaultDisabled, @SerializedName("raised_issues") Collection raisedIssues, @SerializedName("quick_fix_applied") Collection quickFixesApplied) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/CleanAsYouCodePayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload.cayc; import com.google.gson.annotations.SerializedName; public record CleanAsYouCodePayload(@SerializedName("new_code_focus") NewCodeFocusPayload newCodeFocusPayload) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/NewCodeFocusPayload.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload.cayc; public record NewCodeFocusPayload(boolean enabled, int changes) { } ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.payload.cayc; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/package-info.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.telemetry.payload; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClientTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.commons.log.LogOutput.Level; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.rpc.protocol.backend.ai.AiAgent; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingType; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; class TelemetryHttpClientTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private static final String PLATFORM = SystemUtils.OS_NAME; private static final String ARCHITECTURE = SystemUtils.OS_ARCH; private TelemetryHttpClient underTest; @RegisterExtension static WireMockExtension telemetryMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); @BeforeEach void setUp() { InitializeParams initializeParams = mock(InitializeParams.class); when(initializeParams.getTelemetryConstantAttributes()) .thenReturn(new TelemetryClientConstantAttributesDto(null, "product", "version", "ideversion", Map.of("additionalKey", "additionalValue"))); underTest = new TelemetryHttpClient(initializeParams, HttpClientProvider.forTesting(), telemetryMock.baseUrl()); } @Test void opt_out() { telemetryMock.stubFor(delete("/") .willReturn(aResponse())); underTest.optOut(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); await().untilAsserted(() -> telemetryMock.verify(deleteRequestedFor(urlEqualTo("/")) .withRequestBody( equalToJson( "{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + PLATFORM + "\",\"architecture\":\"" + ARCHITECTURE + "\"}", true, true)))); } @Test void upload() { await().untilAsserted(() -> { assertTelemetryUploaded(false); assertThat(logTester.logs(Level.INFO)).noneMatch(l -> l.matches("Sending telemetry payload.")); }); } @Test void upload_with_telemetry_debug_enabled() { await().untilAsserted(() -> { assertTelemetryUploaded(true); assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.matches("Sending telemetry payload.")); assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.contains("{\"days_since_installation\":0,\"days_of_use\":1,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\""+ PLATFORM +"\",\"architecture\":\""+ ARCHITECTURE +"\"")); }); } private void assertTelemetryUploaded(boolean isDebugEnabled) { var spy = spy(underTest); doReturn(isDebugEnabled).when(spy).isTelemetryLogEnabled(); telemetryMock.stubFor(post("/") .willReturn(aResponse())); var telemetryLocalStorage = new TelemetryLocalStorage(); telemetryLocalStorage.helpAndFeedbackLinkClicked("docs"); telemetryLocalStorage.analysisReportingTriggered(AnalysisReportingType.PRE_COMMIT_ANALYSIS_TYPE); telemetryLocalStorage.addQuickFixAppliedForRule("java:S107"); telemetryLocalStorage.addQuickFixAppliedForRule("python:S107"); telemetryLocalStorage.addNewlyFoundIssues(1); telemetryLocalStorage.incrementToolCalledCount("tool_name", true); telemetryLocalStorage.incrementToolCalledCount("tool_name", false); telemetryLocalStorage.addFixedIssues(2); telemetryLocalStorage.findingsFiltered("severity"); telemetryLocalStorage.incrementMcpServerConfigurationRequestedCount(); telemetryLocalStorage.incrementMcpRuleFileRequestedCount(); telemetryLocalStorage.setMcpIntegrationEnabled(true); telemetryLocalStorage.setMcpTransportModeUsed(McpTransportMode.STDIO); telemetryLocalStorage.ideLabsLinkClicked("changed_file_analysis_doc"); telemetryLocalStorage.ideLabsLinkClicked("privacy_policy"); telemetryLocalStorage.ideLabsLinkClicked("privacy_policy"); telemetryLocalStorage.ideLabsFeedbackLinkClicked("connected_mode"); telemetryLocalStorage.ideLabsFeedbackLinkClicked("manage_dependency_risk"); telemetryLocalStorage.ideLabsFeedbackLinkClicked("manage_dependency_risk"); telemetryLocalStorage.aiHookInstalled(AiAgent.WINDSURF); telemetryLocalStorage.aiHookInstalled(AiAgent.WINDSURF); telemetryLocalStorage.campaignShown("feedback_2026_01"); telemetryLocalStorage.campaignResolved("feedback_2026_01", "MAYBE_LATER"); telemetryLocalStorage.campaignShown("feedback_2077_03"); telemetryLocalStorage.campaignResolved("feedback_2077_03", "IGNORE"); telemetryLocalStorage.incrementSupportedLanguagesPanelOpenedCount(); telemetryLocalStorage.incrementSupportedLanguagesPanelOpenedCount(); telemetryLocalStorage.incrementSupportedLanguagesPanelCtaClickedCount(); spy.upload(telemetryLocalStorage, getTelemetryLiveAttributesDto()); telemetryMock.verify(postRequestedFor(urlEqualTo("/")) .withRequestBody( equalToJson( "{\"days_since_installation\":0,\"days_of_use\":1,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + PLATFORM + "\",\"architecture\":\""+ ARCHITECTURE + "\",\"additionalKey\" : \"additionalValue\",\"help_and_feedback\":{\"count_by_link\":{\"docs\":1}}}", true, true))); telemetryMock.verify(postRequestedFor(urlEqualTo("/metrics")) .withRequestBody( equalToJson( String.format(""" {"sonarlint_product":"product","os":"%s","dimension":"installation", "metric_values": [ {"key":"shared_connected_mode.manual","value":"0","type":"integer","granularity":"daily"}, {"key":"help_and_feedback.docs","value":"1","type":"integer","granularity":"daily"}, {"key":"analysis_reporting.trigger_count_pre_commit","value":"1","type":"integer","granularity":"daily"}, {"key":"quick_fix.applied_count","value":"2","type":"integer","granularity":"daily"}, {"key":"connections.attributes","value":"[{\\"userId\\":\\"user-id-sqc\\",\\"organizationId\\":\\"org-id\\"},{\\"serverId\\":\\"server-id\\"}]","type":"string","granularity":"daily"}, {"key":"ide_issues.found","value":"1","type":"integer","granularity":"daily"}, {"key":"ide_issues.fixed","value":"2","type":"integer","granularity":"daily"}, {"key":"tools.tool_name_success_count","value":"1","type":"integer","granularity":"daily"}, {"key":"tools.tool_name_error_count","value":"1","type":"integer","granularity":"daily"}, {"key":"findings_filtered.severity","value":"1","type":"integer","granularity":"daily"}, {"key":"mcp.configuration_requested","value":"1","type":"integer","granularity":"daily"}, {"key":"mcp.rule_file_requested","value":"1","type":"integer","granularity":"daily"}, {"key":"mcp.integration_enabled","value":"true","type":"boolean","granularity":"daily"}, {"key":"mcp.transport_mode","value":"STDIO","type":"string","granularity":"daily"}, {"key":"ide_labs.joined","value":"true","type":"boolean","granularity":"daily"}, {"key":"ide_labs.enabled","value":"false","type":"boolean","granularity":"daily"}, {"key":"ide_labs.link_clicked_count_changed_file_analysis_doc","value":"1","type":"integer","granularity":"daily"}, {"key":"ide_labs.link_clicked_count_privacy_policy","value":"2","type":"integer","granularity":"daily"}, {"key":"ide_labs.feedback_link_clicked_count_connected_mode","value":"1","type":"integer","granularity":"daily"}, {"key":"ide_labs.feedback_link_clicked_count_manage_dependency_risk","value":"2","type":"integer","granularity":"daily"}, {"key":"campaigns.feedback_2026_01_shown", "value":"1", "type": "integer", "granularity":"daily"}, {"key":"campaigns.feedback_2026_01_resolution", "value":"MAYBE_LATER", "type": "string", "granularity":"daily"}, {"key":"campaigns.feedback_2077_03_shown", "value":"1", "type": "integer", "granularity":"daily"}, {"key":"campaigns.feedback_2077_03_resolution", "value":"IGNORE", "type": "string", "granularity":"daily"}, {"key":"ai_hooks.windsurf_installed","value":"2","type":"integer","granularity":"daily"}, {"key":"supported_languages_panel.opened_count","value":"2","type":"integer","granularity":"daily"}, {"key":"supported_languages_panel.cta_clicked_count","value":"1","type":"integer","granularity":"daily"} ]} """, PLATFORM), true, true))); } @Test void should_not_crash_when_cannot_upload() { telemetryMock.stubFor(post("/") .willReturn(aResponse().withStatus(500))); underTest.upload(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); await().untilAsserted(() -> telemetryMock.verify(postRequestedFor(urlEqualTo("/")))); } @Test void should_not_crash_when_cannot_opt_out() { telemetryMock.stubFor(delete("/") .willReturn(aResponse().withStatus(500))); underTest.optOut(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); await().untilAsserted(() -> telemetryMock.verify(deleteRequestedFor(urlEqualTo("/")))); } @Test void failed_upload_should_log_if_debug() { InternalDebug.setEnabled(true); underTest.upload(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); await().untilAsserted(() -> assertThat(logTester.logs(Level.ERROR)).anyMatch(l -> l.matches("Failed to upload telemetry data: .*404.*"))); } @Test void failed_optout_should_log_if_debug() { InternalDebug.setEnabled(true); underTest.optOut(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); await().untilAsserted(() -> assertThat(logTester.logs(Level.ERROR)).anyMatch(l -> l.matches("Failed to upload telemetry opt-out: .*404.*"))); } private static TelemetryLiveAttributes getTelemetryLiveAttributesDto() { var connectionsAttributes = new ArrayList(); connectionsAttributes.add(new TelemetryConnectionAttributes("user-id-sqc", null, "org-id")); connectionsAttributes.add(new TelemetryConnectionAttributes(null, "server-id", null)); var serverAttributes = new TelemetryServerAttributes(true, true, 1, 1, 1, 1, false, Collections.emptyList(), Collections.emptyList(), "3.1.7", connectionsAttributes); var clientAttributes = new TelemetryClientLiveAttributesResponse(Map.of("joinedIdeLabs", true, "enabledIdeLabs", false)); return new TelemetryLiveAttributes(serverAttributes, clientAttributes); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorageManagerTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.nio.file.Path; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.Set; import java.util.UUID; import org.assertj.core.api.Condition; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryMigrationDto; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class TelemetryLocalStorageManagerTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); private final LocalDate today = LocalDate.now(); private Path filePath; @BeforeEach void setUp(@TempDir Path temp) { filePath = temp.resolve("usage"); } @Test void test_default_data() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); var data = storage.tryRead(); assertThat(filePath).doesNotExist(); assertThat(data.installTime()).is(within3SecOfNow); assertThat(data.lastUseDate()).isNull(); assertThat(data.numUseDays()).isZero(); assertThat(data.enabled()).isTrue(); } private final Condition within3SecOfNow = new Condition<>(p -> { var now = OffsetDateTime.now(); return Math.abs(p.until(now, ChronoUnit.SECONDS)) < 3; }, "within3Sec"); @Test void should_update_data() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryRead(); assertThat(filePath).doesNotExist(); storage.tryUpdateAtomically(TelemetryLocalStorage::setUsedAnalysis); assertThat(filePath).exists(); var data2 = storage.tryRead(); assertThat(data2.lastUseDate()).isEqualTo(today); assertThat(data2.numUseDays()).isEqualTo(1); } @Test void should_fix_invalid_installTime() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryUpdateAtomically(data -> { data.setInstallTime(null); data.setNumUseDays(100); }); var data2 = storage.tryRead(); assertThat(data2.installTime()).is(within3SecOfNow); assertThat(data2.lastUseDate()).isNull(); assertThat(data2.numUseDays()).isZero(); } @Test void should_fix_invalid_numDays() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); var tenDaysAgo = OffsetDateTime.now().minusDays(10); storage.tryUpdateAtomically(data -> { data.setInstallTime(tenDaysAgo); data.setLastUseDate(today); data.setNumUseDays(100); }); var data2 = storage.tryRead(); assertThat(data2.installTime()).isEqualTo(tenDaysAgo); assertThat(data2.lastUseDate()).isEqualTo(today); assertThat(data2.numUseDays()).isEqualTo(11); } @Test void should_fix_dates_in_future() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryUpdateAtomically(data -> { data.setInstallTime(OffsetDateTime.now().plusDays(5)); data.setLastUseDate(today.plusDays(7)); data.setNumUseDays(100); }); var data2 = storage.tryRead(); assertThat(data2.installTime()).is(within3SecOfNow); assertThat(data2.lastUseDate()).isEqualTo(today); assertThat(data2.numUseDays()).isEqualTo(1); } @Test void should_not_crash_when_cannot_read_storage(@TempDir Path temp) { InternalDebug.setEnabled(false); assertThatCode(() -> new TelemetryLocalStorageManager(temp, mock(InitializeParams.class)).tryRead()) .doesNotThrowAnyException(); } @Test void should_not_crash_when_cannot_write_storage(@TempDir Path temp) { InternalDebug.setEnabled(false); assertThatCode(() -> new TelemetryLocalStorageManager(temp, mock(InitializeParams.class)).tryUpdateAtomically(d -> {})) .doesNotThrowAnyException(); } @Test void should_increment_open_hotspot_in_browser() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryUpdateAtomically(TelemetryLocalStorage::incrementOpenHotspotInBrowserCount); storage.tryUpdateAtomically(TelemetryLocalStorage::incrementOpenHotspotInBrowserCount); var data2 = storage.tryRead(); assertThat(data2.openHotspotInBrowserCount()).isEqualTo(2); } @Test void should_increment_hotspot_status_changed() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryUpdateAtomically(TelemetryLocalStorage::incrementHotspotStatusChangedCount); storage.tryUpdateAtomically(TelemetryLocalStorage::incrementHotspotStatusChangedCount); storage.tryUpdateAtomically(TelemetryLocalStorage::incrementHotspotStatusChangedCount); var data = storage.tryRead(); assertThat(data.hotspotStatusChangedCount()).isEqualTo(3); } @Test void should_increment_issue_status_changed() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); storage.tryUpdateAtomically(telemetryLocalStorage -> telemetryLocalStorage.addIssueStatusChanged("ruleKey1")); storage.tryUpdateAtomically(telemetryLocalStorage -> telemetryLocalStorage.addIssueStatusChanged("ruleKey2")); var data = storage.tryRead(); assertThat(data.issueStatusChangedCount()).isEqualTo(2); assertThat(data.issueStatusChangedRuleKeys()).containsExactlyInAnyOrder("ruleKey1", "ruleKey2"); } @Test void should_increment_issue_ai_fixable() { var storage = new TelemetryLocalStorageManager(filePath, mock(InitializeParams.class)); var uuid1 = UUID.randomUUID(); var uuid2 = UUID.randomUUID(); var uuid3 = UUID.randomUUID(); storage.tryUpdateAtomically(telemetryLocalStorage -> telemetryLocalStorage.addIssuesWithPossibleAiFixFromIde(Set.of(uuid1, uuid2))); storage.tryUpdateAtomically(telemetryLocalStorage -> telemetryLocalStorage.addIssuesWithPossibleAiFixFromIde(Set.of(uuid1, uuid3))); var data = storage.tryRead(); assertThat(data.getCountIssuesWithPossibleAiFixFromIde()).isEqualTo(3); } @Test void should_migrate_telemetry() { var initializeParams = mock(InitializeParams.class); var expectedInstallTime = OffsetDateTime.now(); when(initializeParams.getTelemetryMigration()).thenReturn(new TelemetryMigrationDto(expectedInstallTime, 42, false)); var storageManager = new TelemetryLocalStorageManager(filePath, initializeParams); var localStorage = storageManager.tryRead(); var actualInstallTime = localStorage.installTime(); var numUseDays = localStorage.numUseDays(); var enabled = localStorage.enabled(); assertThat(enabled).isFalse(); assertThat(numUseDays).isEqualTo(42); assertThat(actualInstallTime).isEqualTo(expectedInstallTime); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorageTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.UUID; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; import static org.sonarsource.sonarlint.core.telemetry.TelemetryLocalStorage.isOlder; class TelemetryLocalStorageTests { @Test void usedAnalysis_should_increment_num_days_on_first_run() { var data = new TelemetryLocalStorage(); assertThat(data.numUseDays()).isZero(); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); } @Test void usedAnalysis_should_not_increment_num_days_on_same_day() { var data = new TelemetryLocalStorage(); assertThat(data.numUseDays()).isZero(); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); } @Test void usedAnalysis_with_duration_should_register_analyzer_performance() { var data = new TelemetryLocalStorage(); assertThat(data.numUseDays()).isZero(); assertThat(data.analyzers()).isEmpty(); data.setUsedAnalysis("java", 1000); data.setUsedAnalysis("js", 2000); assertThat(data.numUseDays()).isEqualTo(1); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); assertThat(data.analyzers()).hasSize(2); assertThat(data.analyzers().get("java").analysisCount()).isEqualTo(1); assertThat(data.analyzers().get("java").frequencies()).containsOnly( entry("0-300", 0), entry("300-500", 0), entry("500-1000", 0), entry("1000-2000", 1), entry("2000-4000", 0), entry("4000+", 0)); assertThat(data.analyzers().get("js").analysisCount()).isEqualTo(1); assertThat(data.analyzers().get("js").frequencies()).containsOnly( entry("0-300", 0), entry("300-500", 0), entry("500-1000", 0), entry("1000-2000", 0), entry("2000-4000", 1), entry("4000+", 0)); } @Test void usedAnalysis_should_increment_num_days_when_day_changed() { var data = new TelemetryLocalStorage(); assertThat(data.numUseDays()).isZero(); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(1); data.setLastUseDate(LocalDate.now().minusDays(1)); data.setUsedAnalysis(); assertThat(data.numUseDays()).isEqualTo(2); } @Test void test_isOlder_LocalDate() { var date = LocalDate.now(); assertThat(isOlder((LocalDate) null, null)).isTrue(); assertThat(isOlder(null, date)).isTrue(); assertThat(isOlder(date, null)).isFalse(); assertThat(isOlder(date, date)).isFalse(); assertThat(isOlder(date, date.plusDays(1))).isTrue(); } @Test void test_isOlder_LocalDateTime() { var date = LocalDateTime.now(); assertThat(isOlder((LocalDateTime) null, null)).isTrue(); assertThat(isOlder(null, date)).isTrue(); assertThat(isOlder(date, null)).isFalse(); assertThat(isOlder(date, date)).isFalse(); assertThat(isOlder(date, date.plusDays(1))).isTrue(); } @Test void validate_should_reset_installTime_if_in_future() { var data = new TelemetryLocalStorage(); var now = OffsetDateTime.now(); data.validateAndMigrate(); assertThat(data.installTime()).is(within3SecOfNow); data.setInstallTime(now.plusDays(1)); data.validateAndMigrate(); assertThat(data.installTime()).is(within3SecOfNow); } private final Condition within3SecOfNow = new Condition<>(p -> { var now = OffsetDateTime.now(); return Math.abs(p.until(now, ChronoUnit.SECONDS)) < 3; }, "within3Sec"); private final Condition about5DaysAgo = new Condition<>(p -> { var fiveDaysAgo = OffsetDateTime.now().minusDays(5); return Math.abs(p.until(fiveDaysAgo, ChronoUnit.SECONDS)) < 3; }, "about5DaysAgo"); @Test void validate_should_reset_lastUseDate_if_in_future() { var data = new TelemetryLocalStorage(); var today = LocalDate.now(); data.setLastUseDate(today.plusDays(1)); data.validateAndMigrate(); assertThat(data.lastUseDate()).isEqualTo(today); } @Test void should_migrate_installDate() { var data = new TelemetryLocalStorage(); data.setInstallDate(LocalDate.now().minusDays(5)); data.validateAndMigrate(); assertThat(data.installTime()).is(about5DaysAgo); } @Test void validate_should_reset_lastUseDate_if_before_installTime() { var data = new TelemetryLocalStorage(); var now = OffsetDateTime.now(); data.setInstallTime(now); data.setLastUseDate(now.minusDays(1).toLocalDate()); data.validateAndMigrate(); assertThat(data.lastUseDate()).isEqualTo(LocalDate.now()); } @Test void validate_should_reset_numDays_if_lastUseDate_unset() { var data = new TelemetryLocalStorage(); data.setNumUseDays(3); data.validateAndMigrate(); assertThat(data.lastUseDate()).isNull(); assertThat(data.numUseDays()).isZero(); } @Test void validate_should_fix_numDays_if_incorrect() { var data = new TelemetryLocalStorage(); var installTime = OffsetDateTime.now().minusDays(10); var lastUseDate = installTime.plusDays(3).toLocalDate(); data.setInstallTime(installTime); data.setLastUseDate(lastUseDate); var numUseDays = installTime.toLocalDate().until(lastUseDate, ChronoUnit.DAYS) + 1; data.setNumUseDays(numUseDays * 2); data.validateAndMigrate(); assertThat(data.numUseDays()).isEqualTo(numUseDays); assertThat(data.installTime()).isEqualTo(installTime); assertThat(data.lastUseDate()).isEqualTo(lastUseDate); } @Test void should_replace_fix_suggestion_snippet_status() { var data = new TelemetryLocalStorage(); var suggestionId = UUID.randomUUID().toString(); data.fixSuggestionResolved(suggestionId, FixSuggestionStatus.ACCEPTED, 0); assertThat(data.getFixSuggestionResolved().get(suggestionId)) .extracting(TelemetryFixSuggestionResolvedStatus::getFixSuggestionResolvedStatus, TelemetryFixSuggestionResolvedStatus::getFixSuggestionResolvedSnippetIndex) .containsExactly(tuple(FixSuggestionStatus.ACCEPTED, 0)); data.fixSuggestionResolved(suggestionId, FixSuggestionStatus.DECLINED, 0); assertThat(data.getFixSuggestionResolved().get(suggestionId)) .extracting(TelemetryFixSuggestionResolvedStatus::getFixSuggestionResolvedStatus, TelemetryFixSuggestionResolvedStatus::getFixSuggestionResolvedSnippetIndex) .containsExactly(tuple(FixSuggestionStatus.DECLINED, 0)); } @Test void should_track_findings_filtered_by_type() { var data = new TelemetryLocalStorage(); assertThat(data.getFindingsFilteredCountersByType()).isEmpty(); data.findingsFiltered("severity"); data.findingsFiltered("severity"); data.findingsFiltered("location"); data.findingsFiltered("fix_availability"); assertThat(data.getFindingsFilteredCountersByType()).hasSize(3); assertThat(data.getFindingsFilteredCountersByType().get("location").getFindingsFilteredCount()).isEqualTo(1); assertThat(data.getFindingsFilteredCountersByType().get("severity").getFindingsFilteredCount()).isEqualTo(2); assertThat(data.getFindingsFilteredCountersByType().get("fix_availability").getFindingsFilteredCount()).isEqualTo(1); } @Test void should_clear_findings_filtered_counters() { var data = new TelemetryLocalStorage(); data.findingsFiltered("severity"); data.findingsFiltered("location"); assertThat(data.getFindingsFilteredCountersByType()).hasSize(2); data.clearAfterPing(); assertThat(data.getFindingsFilteredCountersByType()).isEmpty(); } @Test void should_track_dependency_risk_investigated() { var data = new TelemetryLocalStorage(); assertThat(data.getDependencyRiskInvestigatedRemotelyCount()).isZero(); assertThat(data.getDependencyRiskInvestigatedLocallyCount()).isZero(); data.incrementDependencyRiskInvestigatedRemotelyCount(); data.incrementDependencyRiskInvestigatedLocallyCount(); assertThat(data.getDependencyRiskInvestigatedRemotelyCount()).isEqualTo(1); assertThat(data.getDependencyRiskInvestigatedLocallyCount()).isEqualTo(1); data.incrementDependencyRiskInvestigatedRemotelyCount(); data.incrementDependencyRiskInvestigatedLocallyCount(); assertThat(data.getDependencyRiskInvestigatedRemotelyCount()).isEqualTo(2); assertThat(data.getDependencyRiskInvestigatedLocallyCount()).isEqualTo(2); } @Test void should_clear_dependency_risk_investigated_counts_after_ping() { var data = new TelemetryLocalStorage(); data.incrementDependencyRiskInvestigatedRemotelyCount(); data.incrementDependencyRiskInvestigatedLocallyCount(); assertThat(data.getDependencyRiskInvestigatedRemotelyCount()).isEqualTo(1); assertThat(data.getDependencyRiskInvestigatedLocallyCount()).isEqualTo(1); data.clearAfterPing(); assertThat(data.getDependencyRiskInvestigatedRemotelyCount()).isZero(); assertThat(data.getDependencyRiskInvestigatedLocallyCount()).isZero(); } @Test void should_increment_new_bindings_counters_per_origin() { var data = new TelemetryLocalStorage(); assertThat(data.getNewBindingsPropertiesFileCount()).isZero(); assertThat(data.getNewBindingsRemoteUrlCount()).isZero(); assertThat(data.getNewBindingsProjectNameCount()).isZero(); assertThat(data.getNewBindingsSharedConfigurationCount()).isZero(); data.incrementNewBindingsPropertiesFileCount(); data.incrementNewBindingsRemoteUrlCount(); data.incrementNewBindingsProjectNameCount(); data.incrementNewBindingsSharedConfigurationCount(); assertThat(data.getNewBindingsPropertiesFileCount()).isEqualTo(1); assertThat(data.getNewBindingsRemoteUrlCount()).isEqualTo(1); assertThat(data.getNewBindingsProjectNameCount()).isEqualTo(1); assertThat(data.getNewBindingsSharedConfigurationCount()).isEqualTo(1); } @Test void should_reset_new_bindings_counters_on_clear_after_ping() { var data = new TelemetryLocalStorage(); data.incrementNewBindingsPropertiesFileCount(); data.incrementNewBindingsRemoteUrlCount(); data.incrementNewBindingsProjectNameCount(); data.incrementNewBindingsSharedConfigurationCount(); data.clearAfterPing(); assertThat(data.getNewBindingsPropertiesFileCount()).isZero(); assertThat(data.getNewBindingsRemoteUrlCount()).isZero(); assertThat(data.getNewBindingsProjectNameCount()).isZero(); assertThat(data.getNewBindingsSharedConfigurationCount()).isZero(); } @Test void should_increment_suggested_remote_bindings_count() { var data = new TelemetryLocalStorage(); assertThat(data.getSuggestedRemoteBindingsCount()).isZero(); data.incrementSuggestedRemoteBindingsCount(); data.incrementSuggestedRemoteBindingsCount(); assertThat(data.getSuggestedRemoteBindingsCount()).isEqualTo(2); } @Test void should_increment_mcp_server_settings_requested_count() { var data = new TelemetryLocalStorage(); assertThat(data.getMcpServerConfigurationRequestedCount()).isZero(); data.incrementMcpServerConfigurationRequestedCount(); data.incrementMcpServerConfigurationRequestedCount(); assertThat(data.getMcpServerConfigurationRequestedCount()).isEqualTo(2); } @Test void should_find_mcp_integration_enabled() { var data = new TelemetryLocalStorage(); assertThat(data.isMcpIntegrationEnabled()).isFalse(); data.setMcpIntegrationEnabled(true); assertThat(data.isMcpIntegrationEnabled()).isTrue(); } @Test void should_find_mcp_transport_mode_used() { var data = new TelemetryLocalStorage(); assertThat(data.getMcpTransportModeUsed()).isNull(); data.setMcpTransportModeUsed(McpTransportMode.HTTP); assertThat(data.getMcpTransportModeUsed()).isEqualTo(McpTransportMode.HTTP); } @Test void should_increment_link_clicked_count_for_each_link_separately() { var data = new TelemetryLocalStorage(); assertThat(data.getLabsLinkClickedCount()).isEmpty(); data.ideLabsLinkClicked("1"); data.ideLabsLinkClicked("2"); data.ideLabsLinkClicked("2"); assertThat(data.getLabsLinkClickedCount()) .isEqualTo(Map.of( "1", 1, "2", 2)); } @Test void should_increment_feedback_link_clicked_count_for_each_link_separately() { var data = new TelemetryLocalStorage(); assertThat(data.getLabsFeedbackLinkClickedCount()).isEmpty(); data.ideLabsFeedbackLinkClicked("1"); data.ideLabsFeedbackLinkClicked("2"); data.ideLabsFeedbackLinkClicked("2"); assertThat(data.getLabsFeedbackLinkClickedCount()) .isEqualTo(Map.of( "1", 1, "2", 2)); } @Test void should_reset_link_clicked_data_after_ping() { var data = new TelemetryLocalStorage(); data.ideLabsLinkClicked("1"); data.ideLabsLinkClicked("2"); data.ideLabsFeedbackLinkClicked("1"); data.ideLabsFeedbackLinkClicked("2"); data.clearAfterPing(); assertThat(data.getLabsLinkClickedCount()).isEmpty(); assertThat(data.getLabsFeedbackLinkClickedCount()).isEmpty(); } @Test void should_track_supported_languages_panel_opened() { var data = new TelemetryLocalStorage(); assertThat(data.getSupportedLanguagesPanelOpenedCount()).isZero(); data.incrementSupportedLanguagesPanelOpenedCount(); data.incrementSupportedLanguagesPanelOpenedCount(); assertThat(data.getSupportedLanguagesPanelOpenedCount()).isEqualTo(2); } @Test void should_track_supported_languages_panel_cta_clicked() { var data = new TelemetryLocalStorage(); assertThat(data.getSupportedLanguagesPanelCtaClickedCount()).isZero(); data.incrementSupportedLanguagesPanelCtaClickedCount(); data.incrementSupportedLanguagesPanelCtaClickedCount(); data.incrementSupportedLanguagesPanelCtaClickedCount(); assertThat(data.getSupportedLanguagesPanelCtaClickedCount()).isEqualTo(3); } @Test void should_clear_supported_languages_panel_counts_after_ping() { var data = new TelemetryLocalStorage(); data.incrementSupportedLanguagesPanelOpenedCount(); data.incrementSupportedLanguagesPanelCtaClickedCount(); assertThat(data.getSupportedLanguagesPanelOpenedCount()).isEqualTo(1); assertThat(data.getSupportedLanguagesPanelCtaClickedCount()).isEqualTo(1); data.clearAfterPing(); assertThat(data.getSupportedLanguagesPanelOpenedCount()).isZero(); assertThat(data.getSupportedLanguagesPanelCtaClickedCount()).isZero(); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManagerTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.nio.file.Path; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.mockito.stubbing.Answer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisReportingType.PRE_COMMIT_ANALYSIS_TYPE; class TelemetryManagerTests { private static final int DEFAULT_NOTIF_CLICKED = 5; private static final int DEFAULT_NOTIF_COUNT = 10; private static final int DEFAULT_HELP_AND_FEEDBACK_COUNT = 12; private static final int DEFAULT_ANALYSIS_REPORTING_COUNT = 16; private static final String FOO_EVENT = "foo_event"; private static final String SUGGEST_FEATURE = "suggestFeature"; private TelemetryHttpClient client; private TelemetryManager telemetryManager; private TelemetryLocalStorageManager storageManager; @BeforeEach void setUp(@TempDir Path temp) { client = mock(TelemetryHttpClient.class); storageManager = new TelemetryLocalStorageManager(temp.resolve("storage"), mock(InitializeParams.class)); telemetryManager = new TelemetryManager(storageManager, client); } @Test void enable_should_trigger_upload_once_per_day() { var telemetryPayload = getTelemetryLiveAttributesDto(); telemetryManager.enable(telemetryPayload); telemetryManager.enable(telemetryPayload); verify(client).upload(any(TelemetryLocalStorage.class), eq(telemetryPayload)); verifyNoMoreInteractions(client); } @Test void disable_should_trigger_optout() { var mockStorageManager = mockTelemetryStorage(); var manager = new TelemetryManager(mockStorageManager, client); var telemetryPayload = getTelemetryLiveAttributesDto(); manager.disable(telemetryPayload); verify(client).optOut(any(TelemetryLocalStorage.class), eq(telemetryPayload)); verifyNoMoreInteractions(client); } @Test void uploadAndClearTelemetry_should_trigger_upload_once_per_day() { var telemetryPayload = getTelemetryLiveAttributesDto(); storageManager.tryUpdateAtomically(d -> d.setUsedAnalysis("java", 1000)); var data = storageManager.tryRead(); assertThat(data.analyzers()).isNotEmpty(); assertThat(data.lastUploadTime()).isNull(); telemetryManager.uploadAndClearTelemetry(telemetryPayload); var reloaded = storageManager.tryRead(); // should reset performance after upload assertThat(reloaded.analyzers()).isEmpty(); var lastUploadTime = reloaded.lastUploadTime(); assertThat(lastUploadTime).isNotNull(); telemetryManager.uploadAndClearTelemetry(telemetryPayload); reloaded = storageManager.tryRead(); assertThat(reloaded.lastUploadTime()).isEqualTo(lastUploadTime); verify(client).upload(any(TelemetryLocalStorage.class), eq(telemetryPayload)); verifyNoMoreInteractions(client); } @Test void uploadAndClearTelemetry_should_trigger_upload_if_day_changed_and_hours_elapsed() { var telemetryPayload = getTelemetryLiveAttributesDto(); createAndSaveSampleData(storageManager); storageManager.tryUpdateAtomically(telemetryLocalStorage -> telemetryLocalStorage.setEnabled(true)); telemetryManager.uploadAndClearTelemetry(telemetryPayload); var data = storageManager.tryRead(); var lastUploadTime = data.lastUploadTime() .minusDays(1) .minusHours(TelemetryManager.MIN_HOURS_BETWEEN_UPLOAD); storageManager.tryUpdateAtomically(d -> d.setLastUploadTime(lastUploadTime)); telemetryManager.uploadAndClearTelemetry(telemetryPayload); verify(client, times(2)).upload(any(TelemetryLocalStorage.class), eq(telemetryPayload)); verifyNoMoreInteractions(client); } @Test void uploadAndClearTelemetry_should_not_trigger_upload_if_telemetry_disabled_by_user() { createAndSaveSampleData(storageManager); TelemetryLiveAttributes telemetryLiveAttributesDto = getTelemetryLiveAttributesDto(); telemetryManager.uploadAndClearTelemetry(telemetryLiveAttributesDto); assertThat(storageManager.isEnabled()).isFalse(); verify(client, never()).upload(any(TelemetryLocalStorage.class), eq(telemetryLiveAttributesDto)); verifyNoMoreInteractions(client); } @Test void updateTelemetry_should_not_trigger_update_if_telemetry_disabled_by_user() { createAndSaveSampleData(storageManager); telemetryManager.updateTelemetry(telemetryLocalStorage -> telemetryLocalStorage.setNumUseDays(10)); TelemetryLocalStorage localStorage = storageManager.tryRead(); assertThat(localStorage.enabled()).isFalse(); assertThat(localStorage.numUseDays()).isEqualTo(5); } @Test void enable_should_not_wipe_out_more_recent_data() { var telemetryLiveAttributes = getTelemetryLiveAttributesDto(); createAndSaveSampleData(storageManager); var data = storageManager.tryRead(); assertThat(data.enabled()).isFalse(); // note: the manager hasn't seen the saved data telemetryManager.enable(telemetryLiveAttributes); var reloaded = storageManager.tryRead(); assertThat(reloaded.enabled()).isTrue(); assertThat(reloaded.installTime()).isEqualTo(data.installTime().truncatedTo(ChronoUnit.MILLIS)); assertThat(reloaded.lastUseDate()).isEqualTo(data.lastUseDate()); assertThat(reloaded.numUseDays()).isEqualTo(data.numUseDays()); } @Test void disable_should_not_wipe_out_more_recent_data() { var telemetryPayload = getTelemetryLiveAttributesDto(); createAndSaveSampleData(storageManager); storageManager.tryUpdateAtomically(data -> data.setEnabled(true)); var data = storageManager.tryRead(); assertThat(data.enabled()).isTrue(); // note: the manager hasn't seen the saved data telemetryManager.disable(telemetryPayload); var reloaded = storageManager.tryRead(); assertThat(reloaded.enabled()).isFalse(); assertThat(reloaded.installTime()).isEqualTo(data.installTime()); assertThat(reloaded.lastUseDate()).isEqualTo(data.lastUseDate()); assertThat(reloaded.numUseDays()).isEqualTo(data.numUseDays()); assertThat(reloaded.lastUploadTime()).isEqualTo(data.lastUploadTime()); assertThat(reloaded.notifications().get(FOO_EVENT).getDevNotificationsCount()).isEqualTo(data.notifications().get(FOO_EVENT).getDevNotificationsCount()); assertThat(reloaded.getHelpAndFeedbackLinkClickedCounter().get(SUGGEST_FEATURE).getHelpAndFeedbackLinkClickedCount()) .isEqualTo(data.getHelpAndFeedbackLinkClickedCounter().get(SUGGEST_FEATURE).getHelpAndFeedbackLinkClickedCount()); assertThat(reloaded.getAnalysisReportingCountersByType().get(PRE_COMMIT_ANALYSIS_TYPE).getAnalysisReportingCount()) .isEqualTo(data.getAnalysisReportingCountersByType().get(PRE_COMMIT_ANALYSIS_TYPE).getAnalysisReportingCount()); } @Test void uploadAndClearTelemetry_should_clear_accumulated_data() { var telemetryPayload = getTelemetryLiveAttributesDto(); createAndSaveSampleData(storageManager); storageManager.tryUpdateAtomically(data -> { data.setEnabled(true); data.setUsedAnalysis("java", 1000); data.incrementHotspotStatusChangedCount(); data.incrementOpenHotspotInBrowserCount(); data.incrementShowHotspotRequestCount(); data.incrementShowIssueRequestCount(); data.addIssuesWithPossibleAiFixFromIde(Set.of(UUID.randomUUID(), UUID.randomUUID())); data.fixSuggestionReceived("suggestionId", AiSuggestionSource.SONARCLOUD, 2, true); data.fixSuggestionResolved("suggestionId", FixSuggestionStatus.ACCEPTED, 0); data.incrementTaintVulnerabilitiesInvestigatedLocallyCount(); data.incrementTaintVulnerabilitiesInvestigatedRemotelyCount(); data.setLastUploadTime(LocalDateTime.now().minusDays(2)); data.setNumUseDays(5); data.notifications().put(FOO_EVENT, new TelemetryNotificationsCounter(DEFAULT_NOTIF_COUNT, DEFAULT_NOTIF_CLICKED)); data.getHelpAndFeedbackLinkClickedCounter().put(SUGGEST_FEATURE, new TelemetryHelpAndFeedbackCounter(DEFAULT_HELP_AND_FEEDBACK_COUNT)); data.getAnalysisReportingCountersByType().put(PRE_COMMIT_ANALYSIS_TYPE, new TelemetryAnalysisReportingCounter(DEFAULT_ANALYSIS_REPORTING_COUNT)); data.findingsFiltered("severity"); data.setMcpIntegrationEnabled(true); data.setMcpTransportModeUsed(McpTransportMode.HTTPS); }); telemetryManager.uploadAndClearTelemetry(telemetryPayload); var reloaded = storageManager.tryRead(); assertThat(reloaded.analyzers()).isEmpty(); assertThat(reloaded.showHotspotRequestsCount()).isZero(); assertThat(reloaded.notifications()).isEmpty(); assertThat(reloaded.taintVulnerabilitiesInvestigatedLocallyCount()).isZero(); assertThat(reloaded.taintVulnerabilitiesInvestigatedRemotelyCount()).isZero(); assertThat(reloaded.hotspotStatusChangedCount()).isZero(); assertThat(reloaded.getShowIssueRequestsCount()).isZero(); assertThat(reloaded.getCountIssuesWithPossibleAiFixFromIde()).isZero(); assertThat(reloaded.getFixSuggestionReceivedCounter()).isEmpty(); assertThat(reloaded.getFixSuggestionResolved()).isEmpty(); assertThat(reloaded.openHotspotInBrowserCount()).isZero(); assertThat(reloaded.getHelpAndFeedbackLinkClickedCounter()).isEmpty(); assertThat(reloaded.getAnalysisReportingCountersByType()).isEmpty(); assertThat(reloaded.getFindingsFilteredCountersByType()).isEmpty(); assertThat(reloaded.isMcpIntegrationEnabled()).isFalse(); assertThat(reloaded.getMcpTransportModeUsed()).isNull(); } private void createAndSaveSampleData(TelemetryLocalStorageManager storage) { storage.tryUpdateAtomically(data -> { data.setEnabled(false); data.setInstallTime(OffsetDateTime.now().minusDays(10)); data.setLastUseDate(LocalDate.now().minusDays(3)); data.setLastUploadTime(LocalDateTime.now().minusDays(2)); data.setNumUseDays(5); data.notifications().put(FOO_EVENT, new TelemetryNotificationsCounter(DEFAULT_NOTIF_COUNT, DEFAULT_NOTIF_CLICKED)); data.getHelpAndFeedbackLinkClickedCounter().put(SUGGEST_FEATURE, new TelemetryHelpAndFeedbackCounter(DEFAULT_HELP_AND_FEEDBACK_COUNT)); data.getAnalysisReportingCountersByType().put(PRE_COMMIT_ANALYSIS_TYPE, new TelemetryAnalysisReportingCounter(DEFAULT_ANALYSIS_REPORTING_COUNT)); }); } private TelemetryLocalStorageManager mockTelemetryStorage() { var storage = mock(TelemetryLocalStorageManager.class); when(storage.tryRead()).thenReturn(new TelemetryLocalStorage()); doAnswer((Answer) invocation -> { var args = invocation.getArguments(); ((Consumer) args[0]).accept(mock(TelemetryLocalStorage.class)); return null; }).when(storage).tryUpdateAtomically(any(Consumer.class)); return storage; } private static TelemetryLiveAttributes getTelemetryLiveAttributesDto() { var serverAttributes = new TelemetryServerAttributes(true, true, 1, 1, 1, 0, false, Collections.emptyList(), Collections.emptyList(), "3.1.7", Collections.emptyList()); var clientAttributes = new TelemetryClientLiveAttributesResponse(emptyMap()); return new TelemetryLiveAttributes(serverAttributes, clientAttributes); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtilsTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.sonarsource.sonarlint.core.telemetry.TelemetryUtils.isGracePeriodElapsedAndDayChanged; class TelemetryUtilsTests { @Test void dayChanged_should_return_true_for_null() { assertThat(isGracePeriodElapsedAndDayChanged(null)).isTrue(); } @Test void dayChanged_should_return_true_if_older() { assertThat(isGracePeriodElapsedAndDayChanged(LocalDate.now().minusDays(1))).isTrue(); } @Test void should_create_telemetry_performance_payload() { Map analyzers = new HashMap<>(); var perf = new TelemetryAnalyzerPerformance(); perf.registerAnalysis(10); perf.registerAnalysis(500); perf.registerAnalysis(500); analyzers.put("java", perf); var payload = TelemetryUtils.toPayload(analyzers); assertThat(payload).hasSize(1); assertThat(payload[0].language()).isEqualTo("java"); assertThat(payload[0].distribution()).containsOnly( entry("0-300", new BigDecimal("33.33")), entry("300-500", new BigDecimal("0.00")), entry("500-1000", new BigDecimal("66.67")), entry("1000-2000", new BigDecimal("0.00")), entry("2000-4000", new BigDecimal("0.00")), entry("4000+", new BigDecimal("0.00"))); } @Test void dayChanged_should_return_false_if_same() { assertThat(isGracePeriodElapsedAndDayChanged(LocalDate.now())).isFalse(); } @Test void dayChanged_with_hours_should_return_true_for_null() { assertThat(TelemetryUtils.isGracePeriodElapsedAndDayChanged(null, 1)).isTrue(); } @Test void dayChanged_with_hours_should_return_false_if_day_same() { assertThat(TelemetryUtils.isGracePeriodElapsedAndDayChanged(LocalDateTime.now(), 100)).isFalse(); } @Test void create_analyzer_performance_payload() { var perf = new TelemetryAnalyzerPerformance(); for (var i = 0; i < 10; i++) { perf.registerAnalysis(1000); } for (var i = 0; i < 20; i++) { perf.registerAnalysis(2000); } for (var i = 0; i < 20; i++) { perf.registerAnalysis(200); } assertThat(perf.analysisCount()).isEqualTo(50); var payload = TelemetryUtils.toPayload(Collections.singletonMap("java", perf)); assertThat(payload).hasSize(1); assertThat(payload[0].language()).isEqualTo("java"); assertThat(payload[0].distribution()).containsExactly( entry("0-300", new BigDecimal("40.00")), entry("300-500", new BigDecimal("0.00")), entry("500-1000", new BigDecimal("0.00")), entry("1000-2000", new BigDecimal("20.00")), entry("2000-4000", new BigDecimal("40.00")), entry("4000+", new BigDecimal("0.00"))); } @Test void dayChanged_with_hours_should_return_false_if_different_day_but_within_hours() { var date = LocalDateTime.now().minusDays(1); var hours = date.until(LocalDateTime.now(), ChronoUnit.HOURS); assertThat(TelemetryUtils.isGracePeriodElapsedAndDayChanged(date, hours + 1)).isFalse(); } @Test void dayChanged_with_hours_should_return_true_if_different_day_and_beyond_hours() { var date = LocalDateTime.now().minusDays(1); var hours = date.until(LocalDateTime.now(), ChronoUnit.HOURS); assertThat(TelemetryUtils.isGracePeriodElapsedAndDayChanged(date, hours)).isTrue(); } @Test void should_create_telemetry_fixSuggestions_payload() { var suggestionId1 = UUID.randomUUID().toString(); var counter1 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 4, true); var suggestionId2 = UUID.randomUUID().toString(); var counter2 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 2, true); var suggestionId3 = UUID.randomUUID().toString(); var counter3 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 1, false); var fixSuggestionReceivedCounter = Map.of( suggestionId1, counter1, suggestionId2, counter2, suggestionId3, counter3 ); var fixSuggestionResolvedStatus1 = new TelemetryFixSuggestionResolvedStatus(FixSuggestionStatus.ACCEPTED, 0); var fixSuggestionResolvedStatus2 = new TelemetryFixSuggestionResolvedStatus(FixSuggestionStatus.ACCEPTED, 1); var fixSuggestionResolvedStatus3 = new TelemetryFixSuggestionResolvedStatus(FixSuggestionStatus.DECLINED, null); var fixSuggestionResolved = Map.of(suggestionId1, List.of(fixSuggestionResolvedStatus1, fixSuggestionResolvedStatus2), suggestionId3, List.of(fixSuggestionResolvedStatus3)); var result = TelemetryUtils.toFixSuggestionResolvedPayload(fixSuggestionReceivedCounter, fixSuggestionResolved); assertThat(result).hasSize(3); var resultingSuggestion1 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId1)).findFirst().orElseThrow(); assertThat(resultingSuggestion1.suggestionId()).isEqualTo(suggestionId1); assertThat(resultingSuggestion1.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); assertThat(resultingSuggestion1.countSnippets()).isEqualTo(4); assertThat(resultingSuggestion1.snippets()).hasSize(2); assertThat(resultingSuggestion1.wasAiFixSuggestionGeneratedFromIde()).isTrue(); var resultingSuggestion2 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId2)).findFirst().orElseThrow(); assertThat(resultingSuggestion2.suggestionId()).isEqualTo(suggestionId2); assertThat(resultingSuggestion2.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); assertThat(resultingSuggestion2.countSnippets()).isEqualTo(2); assertThat(resultingSuggestion2.snippets()).hasSize(1); assertThat(resultingSuggestion2.snippets().get(0).status()).isNull(); assertThat(resultingSuggestion2.snippets().get(0).snippetIndex()).isNull(); assertThat(resultingSuggestion2.wasAiFixSuggestionGeneratedFromIde()).isTrue(); var resultingSuggestion3 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId3)).findFirst().orElseThrow(); assertThat(resultingSuggestion3.suggestionId()).isEqualTo(suggestionId3); assertThat(resultingSuggestion3.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); assertThat(resultingSuggestion3.countSnippets()).isEqualTo(1); assertThat(resultingSuggestion3.snippets()).hasSize(1); var telemetryFixSuggestionResolvedPayload3 = resultingSuggestion3.snippets().get(0); assertThat(telemetryFixSuggestionResolvedPayload3.snippetIndex()).isNull(); assertThat(telemetryFixSuggestionResolvedPayload3.status()).isEqualTo(FixSuggestionStatus.DECLINED); assertThat(resultingSuggestion3.wasAiFixSuggestionGeneratedFromIde()).isFalse(); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/gessie/GessieHttpClientTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.matching.EqualToPattern; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarsource.sonarlint.core.http.HttpClientProvider; import org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieEvent; import org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata; import org.sonarsource.sonarlint.core.telemetry.gessie.event.payload.MessagePayload; import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.awaitility.Awaitility.await; import static org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata.GessieSource; import static org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata.SonarLintDomain; class GessieHttpClientTests { private static final String IDE_ENDPOINT = "/ide"; private GessieHttpClient tested; @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); @RegisterExtension static WireMockExtension mockGessie = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); @BeforeEach void setUp() { tested = new GessieHttpClient(HttpClientProvider.forTesting(), mockGessie.baseUrl(), "value"); } @Test void should_upload_accepted_payload() throws URISyntaxException, IOException { mockGessie.stubFor(post(IDE_ENDPOINT) .willReturn(aResponse().withStatus(202))); tested.postEvent(getPayload()); var fileContent = getTestJson("GessieRequest"); await().untilAsserted(() -> mockGessie.verify(postRequestedFor(urlEqualTo(IDE_ENDPOINT)) .withHeader("x-api-key", new EqualToPattern("value")) .withRequestBody(equalToJson(fileContent)))); } @Test void should_handle_400_error_gracefully() throws URISyntaxException, IOException { mockGessie.stubFor(post(IDE_ENDPOINT) .willReturn(aResponse().withStatus(400))); tested.postEvent(new GessieEvent(null, null)); var invalidRequest = getTestJson("InvalidRequest"); await().untilAsserted(() -> mockGessie.verify(postRequestedFor(urlEqualTo(IDE_ENDPOINT)) .withHeader("x-api-key", new EqualToPattern("value")) .withRequestBody(equalToJson(invalidRequest)))); } @Test void should_handle_403_error_gracefully() throws URISyntaxException, IOException { mockGessie.stubFor(post(IDE_ENDPOINT) .willReturn(aResponse().withStatus(403))); tested.postEvent(getPayload()); var fileContent = getTestJson("GessieRequest"); await().untilAsserted(() -> mockGessie.verify(postRequestedFor(urlEqualTo(IDE_ENDPOINT)) .withHeader("x-api-key", new EqualToPattern("value")) .withRequestBody(equalToJson(fileContent)))); } private String getTestJson(String fileName) throws URISyntaxException, IOException { var resource = Objects.requireNonNull(getClass().getResource("/response/gessie/GessieHttpClientTest/" + fileName + ".json")) .toURI(); return Files.readString(Path.of(resource)); } private static GessieEvent getPayload() { return new GessieEvent( new GessieMetadata(UUID.fromString("a36e25e8-5a92-4b5d-93b4-ba0045947b4c"), new GessieSource(SonarLintDomain.INTELLIJ), "Analytics.Test.TestEvent", "1761821877867", "0"), new MessagePayload("Test event", "test") ); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/gessie/event/GessieMetadataTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.gessie.event; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.sonarsource.sonarlint.core.telemetry.gessie.event.GessieMetadata.SonarLintDomain; class GessieMetadataTests { @ParameterizedTest @MethodSource void should_map_product_key_to_domain(String productKey, SonarLintDomain expected) { var actual = SonarLintDomain.fromProductKey(productKey); assertThat(actual).isEqualTo(expected); } public static Stream should_map_product_key_to_domain() { return Stream.of( Arguments.of("idea", SonarLintDomain.INTELLIJ), Arguments.of("eclipse", SonarLintDomain.ECLIPSE), Arguments.of("visualstudio", SonarLintDomain.VISUAL_STUDIO), Arguments.of("vscode", SonarLintDomain.VS_CODE), Arguments.of("cursor", SonarLintDomain.VS_CODE), Arguments.of("windsurf", SonarLintDomain.VS_CODE), Arguments.of("", SonarLintDomain.SLCORE), Arguments.of("test", SonarLintDomain.SLCORE) ); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryMeasuresPayloadTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.Gson; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.McpTransportMode; import org.sonarsource.sonarlint.core.telemetry.TelemetryConnectionAttributes; import org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresDimension; import org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresPayload; import org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValue; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueGranularity.DAILY; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.BOOLEAN; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.INTEGER; import static org.sonarsource.sonarlint.core.telemetry.measures.payload.TelemetryMeasuresValueType.STRING; class TelemetryMeasuresPayloadTests { @Test void testGenerationJson() { var messageUuid = "25318599-9aec-4e1d-a535-1bfa4f7fcf39"; var installTime = OffsetDateTime.of(2017, 11, 10, 12, 1, 14, 984_123_123, ZoneOffset.ofHours(2)); var measures = generateMeasures(); var m = new TelemetryMeasuresPayload( messageUuid, "Linux Ubuntu 24.04", installTime, "SonarQube for IDE", TelemetryMeasuresDimension.INSTALLATION, measures ); var s = m.toJson(); assertThat(s).isEqualTo("{" + "\"message_uuid\":\"25318599-9aec-4e1d-a535-1bfa4f7fcf39\"," + "\"os\":\"Linux Ubuntu 24.04\"," + "\"install_time\":\"2017-11-10T12:01:14.984+02:00\"," + "\"sonarlint_product\":\"SonarQube for IDE\"," + "\"dimension\":\"installation\"," + "\"metric_values\":[{" + "\"key\":\"shared_connected_mode.manual\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"shared_connected_mode.imported\",\"value\":\"2\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"shared_connected_mode.auto\",\"value\":\"3\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"shared_connected_mode.exported\",\"value\":\"4\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"new_bindings.manual\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"new_bindings.accepted_suggestion_remote_url\",\"value\":\"2\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"new_bindings.accepted_suggestion_properties_file\",\"value\":\"3\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"new_bindings.accepted_suggestion_shared_config_file\",\"value\":\"4\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"new_bindings.accepted_suggestion_project_name\",\"value\":\"5\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"binding_suggestion_clue.remote_url\",\"value\":\"5\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"bindings.child_count\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"bindings.server_count\",\"value\":\"2\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"bindings.cloud_eu_count\",\"value\":\"0\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"bindings.cloud_us_count\",\"value\":\"0\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"connections.attributes\",\"value\":\"[{\\\"userId\\\":\\\"user-id\\\",\\\"organizationId\\\":\\\"org-id\\\"}]\",\"type\":\"string\",\"granularity\":\"daily\"}," + "{\"key\":\"help_and_feedback.doc_link\",\"value\":\"5\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"analysis_reporting.trigger_count_vcs_changed_files\",\"value\":\"7\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"performance.biggest_size_config_scope_files\",\"value\":\"12345\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"automatic_analysis.enabled\",\"value\":\"true\",\"type\":\"boolean\",\"granularity\":\"daily\"}," + "{\"key\":\"automatic_analysis.toggled_count\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"mcp.configuration_requested\",\"value\":\"3\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"mcp.rule_file_requested\",\"value\":\"4\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"mcp.integration_enabled\",\"value\":\"true\",\"type\":\"boolean\",\"granularity\":\"daily\"}," + "{\"key\":\"mcp.transport_mode\",\"value\":\"HTTP\",\"type\":\"string\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.joined\",\"value\":\"true\",\"type\":\"boolean\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.enabled\",\"value\":\"true\",\"type\":\"boolean\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.link_clicked_count_changed_file_analysis_doc\",\"value\":\"10\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.link_clicked_count_privacy_policy\",\"value\":\"20\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.feedback_link_clicked_count_connected_mode\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"ide_labs.feedback_link_clicked_count_manage_dependency_risk\",\"value\":\"2\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"ai_hooks.windsurf_installed\",\"value\":\"2\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"ai_hooks.cursor_installed\",\"value\":\"5\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"campaigns.feedback_2026_01_shown\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"campaigns.feedback_2026_01_resolution\",\"value\":\"MAYBE_LATER\",\"type\":\"string\",\"granularity\":\"daily\"}," + "{\"key\":\"campaigns.feedback_2077_03_shown\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"campaigns.feedback_2077_03_resolution\",\"value\":\"CLOSED\",\"type\":\"string\",\"granularity\":\"daily\"}," + "{\"key\":\"supported_languages_panel.opened_count\",\"value\":\"3\",\"type\":\"integer\",\"granularity\":\"daily\"}," + "{\"key\":\"supported_languages_panel.cta_clicked_count\",\"value\":\"7\",\"type\":\"integer\",\"granularity\":\"daily\"}" + "]}"); assertThat(m.messageUuid()).isEqualTo(messageUuid); assertThat(m.os()).isEqualTo("Linux Ubuntu 24.04"); assertThat(m.installTime()).isEqualTo(installTime); assertThat(m.product()).isEqualTo("SonarQube for IDE"); assertThat(m.dimension()).isEqualTo(TelemetryMeasuresDimension.INSTALLATION); assertValues(m.values()); } private List generateMeasures() { var values = new ArrayList(); values.add(new TelemetryMeasuresValue("shared_connected_mode.manual", String.valueOf(1), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.imported", String.valueOf(2), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.auto", String.valueOf(3), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("shared_connected_mode.exported", String.valueOf(4), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.manual", String.valueOf(1), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_remote_url", String.valueOf(2), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_properties_file", String.valueOf(3), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_shared_config_file", String.valueOf(4), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("new_bindings.accepted_suggestion_project_name", String.valueOf(5), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("binding_suggestion_clue.remote_url", String.valueOf(5), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.child_count", String.valueOf(1), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.server_count", String.valueOf(2), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.cloud_eu_count", String.valueOf(0), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("bindings.cloud_us_count", String.valueOf(0), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("connections.attributes", new Gson().toJson(List.of(new TelemetryConnectionAttributes("user-id", null, "org-id"))), STRING, DAILY)); values.add(new TelemetryMeasuresValue("help_and_feedback.doc_link", String.valueOf(5), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("analysis_reporting.trigger_count_vcs_changed_files", String.valueOf(7), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("performance.biggest_size_config_scope_files", String.valueOf(12345), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("automatic_analysis.enabled", String.valueOf(true), BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("automatic_analysis.toggled_count", String.valueOf(1), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("mcp.configuration_requested", String.valueOf(3), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("mcp.rule_file_requested", String.valueOf(4), INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("mcp.integration_enabled", String.valueOf(true), BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("mcp.transport_mode", McpTransportMode.HTTP.name(), STRING, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.joined", "true", BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.enabled", "true", BOOLEAN, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.link_clicked_count_changed_file_analysis_doc", "10", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.link_clicked_count_privacy_policy", "20", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.feedback_link_clicked_count_connected_mode", "1", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("ide_labs.feedback_link_clicked_count_manage_dependency_risk", "2", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("ai_hooks.windsurf_installed", "2", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("ai_hooks.cursor_installed", "5", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("campaigns.feedback_2026_01_shown", "1", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("campaigns.feedback_2026_01_resolution", "MAYBE_LATER", STRING, DAILY)); values.add(new TelemetryMeasuresValue("campaigns.feedback_2077_03_shown", "1", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("campaigns.feedback_2077_03_resolution", "CLOSED", STRING, DAILY)); values.add(new TelemetryMeasuresValue("supported_languages_panel.opened_count", "3", INTEGER, DAILY)); values.add(new TelemetryMeasuresValue("supported_languages_panel.cta_clicked_count", "7", INTEGER, DAILY)); return values; } private static void assertValues(List values) { assertThat(values).extracting("key", "value", "type", "granularity") .contains(tuple("shared_connected_mode.manual", "1", INTEGER, DAILY)) .contains(tuple("shared_connected_mode.imported", "2", INTEGER, DAILY)) .contains(tuple("shared_connected_mode.auto", "3", INTEGER, DAILY)) .contains(tuple("shared_connected_mode.exported", "4", INTEGER, DAILY)) .contains(tuple("new_bindings.manual", "1", INTEGER, DAILY)) .contains(tuple("new_bindings.accepted_suggestion_remote_url", "2", INTEGER, DAILY)) .contains(tuple("new_bindings.accepted_suggestion_properties_file", "3", INTEGER, DAILY)) .contains(tuple("new_bindings.accepted_suggestion_shared_config_file", "4", INTEGER, DAILY)) .contains(tuple("new_bindings.accepted_suggestion_project_name", "5", INTEGER, DAILY)) .contains(tuple("binding_suggestion_clue.remote_url", "5", INTEGER, DAILY)) .contains(tuple("connections.attributes", "[{\"userId\":\"user-id\",\"organizationId\":\"org-id\"}]", STRING, DAILY)) .contains(tuple("help_and_feedback.doc_link", "5", INTEGER, DAILY)) .contains(tuple("analysis_reporting.trigger_count_vcs_changed_files", "7", INTEGER, DAILY)) .contains(tuple("automatic_analysis.enabled", "true", BOOLEAN, DAILY)) .contains(tuple("automatic_analysis.toggled_count", "1", INTEGER, DAILY)) .contains(tuple("mcp.configuration_requested", "3", INTEGER, DAILY)) .contains(tuple("mcp.rule_file_requested", "4", INTEGER, DAILY)) .contains(tuple("mcp.integration_enabled", "true", BOOLEAN, DAILY)) .contains(tuple("mcp.transport_mode", "HTTP", STRING, DAILY)) .contains(tuple("ide_labs.joined", "true", BOOLEAN, DAILY)) .contains(tuple("ide_labs.enabled", "true", BOOLEAN, DAILY)) .contains(tuple("ide_labs.link_clicked_count_changed_file_analysis_doc", "10", INTEGER, DAILY)) .contains(tuple("ide_labs.link_clicked_count_privacy_policy", "20", INTEGER, DAILY)) .contains(tuple("ide_labs.feedback_link_clicked_count_connected_mode", "1", INTEGER, DAILY)) .contains(tuple("ide_labs.feedback_link_clicked_count_manage_dependency_risk", "2", INTEGER, DAILY)) .contains(tuple("ai_hooks.windsurf_installed", "2", INTEGER, DAILY)) .contains(tuple("ai_hooks.cursor_installed", "5", INTEGER, DAILY)) .contains(tuple("campaigns.feedback_2026_01_shown", "1", INTEGER, DAILY)) .contains(tuple("campaigns.feedback_2026_01_resolution", "MAYBE_LATER", STRING, DAILY)) .contains(tuple("campaigns.feedback_2077_03_shown", "1", INTEGER, DAILY)) .contains(tuple("campaigns.feedback_2077_03_resolution", "CLOSED", STRING, DAILY)) .contains(tuple("supported_languages_panel.opened_count", "3", INTEGER, DAILY)) .contains(tuple("supported_languages_panel.cta_clicked_count", "7", INTEGER, DAILY)); } } ================================================ FILE: backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayloadTests.java ================================================ /* * SonarLint Core - Telemetry * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.telemetry.payload; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.math.BigDecimal; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; import org.sonarsource.sonarlint.core.telemetry.TelemetryHelpAndFeedbackCounter; import org.sonarsource.sonarlint.core.telemetry.payload.cayc.CleanAsYouCodePayload; import org.sonarsource.sonarlint.core.telemetry.payload.cayc.NewCodeFocusPayload; import static org.assertj.core.api.Assertions.assertThat; class TelemetryPayloadTests { @Test void testGenerationJson() { var installTime = OffsetDateTime.of(2017, 11, 10, 12, 1, 14, 984_123_123, ZoneOffset.ofHours(2)); var systemTime = installTime.plusMinutes(1); var perf = new TelemetryAnalyzerPerformancePayload[1]; Map distrib = new LinkedHashMap<>(); distrib.put("0-300", BigDecimal.valueOf(9.90)); distrib.put("1000-2000", BigDecimal.valueOf(90.10)); perf[0] = new TelemetryAnalyzerPerformancePayload("java", distrib); Map counters = new HashMap<>(); counters.put("QUALITY_GATE", new TelemetryNotificationsCounterPayload(5, 3)); counters.put("NEW_ISSUES", new TelemetryNotificationsCounterPayload(10, 1)); var notifPayload = new TelemetryNotificationsPayload(true, counters); var showHotspotPayload = new ShowHotspotPayload(4); var showIssuePayload = new ShowIssuePayload(3); var hotspotPayload = new HotspotPayload(5, 3); var taintVulnerabilitiesPayload = new TaintVulnerabilitiesPayload(6, 7); var issuePayload = new IssuePayload(Set.of("java:S123"), 1); var rulesPayload = new TelemetryRulesPayload(Arrays.asList("enabledRuleKey1", "enabledRuleKey2"), Arrays.asList("disabledRuleKey1", "disabledRuleKey2"), Arrays.asList("reportedRuleKey1", "reportedRuleKey2"), Arrays.asList("quickFixedRuleKey1", "quickFixedRuleKey2")); Map helpAndFeedbackCounter = new HashMap<>(); helpAndFeedbackCounter.put("faq", new TelemetryHelpAndFeedbackCounter(4)); helpAndFeedbackCounter.put("docs", new TelemetryHelpAndFeedbackCounter(5)); var helpAndFeedbackPayload = new TelemetryHelpAndFeedbackPayload(helpAndFeedbackCounter); var aiFixSuggestionsPayload = getTelemetryFixSuggestionPayloads(); Map additionalProps = new LinkedHashMap<>(); additionalProps.put("aString", "stringValue"); additionalProps.put("aBool", false); additionalProps.put("aNumber", 1.5); var sharedConnectedModePayload = new ShareConnectedModePayload(3, 2, 1, 4); Map additionalPropsSub = new LinkedHashMap<>(); additionalPropsSub.put("aSubNumber", 2); additionalProps.put("sub", additionalPropsSub); var cleanAsYouCodePayload = new CleanAsYouCodePayload(new NewCodeFocusPayload(true, 2)); var m = new TelemetryPayload(4, 15, "SLI", "2.4", "Pycharm 3.2", "platform", "architecture", true, true, systemTime, installTime, "Windows 10", "1.8.0", "10.5.2", perf, notifPayload, showHotspotPayload, showIssuePayload, taintVulnerabilitiesPayload, rulesPayload, hotspotPayload, issuePayload, helpAndFeedbackPayload, aiFixSuggestionsPayload, 1, cleanAsYouCodePayload, sharedConnectedModePayload, additionalProps); var s = m.toJson(); assertThat(s).isEqualTo("{\"days_since_installation\":4," + "\"days_of_use\":15," + "\"sonarlint_version\":\"2.4\"," + "\"sonarlint_product\":\"SLI\"," + "\"ide_version\":\"Pycharm 3.2\"," + "\"platform\":\"platform\"," + "\"architecture\":\"architecture\"," + "\"connected_mode_used\":true," + "\"connected_mode_sonarcloud\":true," + "\"system_time\":\"2017-11-10T12:02:14.984+02:00\"," + "\"install_time\":\"2017-11-10T12:01:14.984+02:00\"," + "\"os\":\"Windows 10\"," + "\"jre\":\"1.8.0\"," + "\"nodejs\":\"10.5.2\"," + "\"analyses\":[{\"language\":\"java\",\"rate_per_duration\":{\"0-300\":9.9,\"1000-2000\":90.1}}]," + "\"server_notifications\":{\"disabled\":true,\"count_by_type\":{\"NEW_ISSUES\":{\"received\":10,\"clicked\":1},\"QUALITY_GATE\":{\"received\":5,\"clicked\":3}}}," + "\"show_hotspot\":{\"requests_count\":4}," + "\"show_issue\":{\"requests_count\":3}," + "\"taint_vulnerabilities\":{\"investigated_locally_count\":6,\"investigated_remotely_count\":7}," + "\"rules\":{\"non_default_enabled\":[\"enabledRuleKey1\",\"enabledRuleKey2\"],\"default_disabled\":[\"disabledRuleKey1\",\"disabledRuleKey2\"],\"raised_issues\":[\"reportedRuleKey1\",\"reportedRuleKey2\"],\"quick_fix_applied\":[\"quickFixedRuleKey1\",\"quickFixedRuleKey2\"]}," + "\"hotspot\":{\"open_in_browser_count\":5,\"status_changed_count\":3}," + "\"issue\":{\"status_changed_rule_keys\":[\"java:S123\"],\"status_changed_count\":1}," + "\"help_and_feedback\":{\"count_by_link\":{\"docs\":5,\"faq\":4}}," + "\"ai_fix_suggestions\":[{\"suggestion_id\":\"suggestionId1\",\"count_snippets\":1,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":0},{\"status\":\"DECLINED\",\"snippet_index\":1}],\"was_ai_fix_suggestion_generated_from_ide\":true},{\"suggestion_id\":\"suggestionId2\",\"count_snippets\":2,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":null}],\"was_ai_fix_suggestion_generated_from_ide\":true},{\"suggestion_id\":\"suggestionId3\",\"count_snippets\":3,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":null,\"snippet_index\":null}],\"was_ai_fix_suggestion_generated_from_ide\":false}]," + "\"count_issues_with_possible_ai_fix_from_ide\":1," + "\"cayc\":{\"new_code_focus\":{\"enabled\":true,\"changes\":2}}," + "\"shared_connected_mode\":{\"manual_bindings_count\":3,\"imported_bindings_count\":2,\"auto_bindings_count\":1,\"exported_connected_mode_count\":4}," + "\"aString\":\"stringValue\"," + "\"aBool\":false," + "\"aNumber\":1.5," + "\"sub\":{\"aSubNumber\":2}}"); assertThat(m.connectedMode()).isTrue(); assertThat(m.analyses()).hasSize(1); assertThat(m.connectedModeSonarcloud()).isTrue(); assertThat(m.systemTime()).isEqualTo(systemTime); assertThat(m.notifications().disabled()).isTrue(); assertThat(m.notifications().counters()).containsOnlyKeys("QUALITY_GATE", "NEW_ISSUES"); assertThat(m.helpAndFeedbackPayload().getCounters()).containsOnlyKeys("docs", "faq"); assertThat(m.getAiFixSuggestionsPayload()).hasSize(3); assertThat(m.getCountIssuesWithPossibleAiFixFromIde()).isEqualTo(1); assertThat(m.cleanAsYouCodePayload().newCodeFocusPayload()) .extracting(NewCodeFocusPayload::enabled, NewCodeFocusPayload::changes) .containsExactly(true, 2); assertThat(m.issuePayload().statusChangedRuleKeys()).isEqualTo(Set.of("java:S123")); assertThat(m.issuePayload().statusChangedCount()).isEqualTo(1); assertThat(m.additionalAttributes()).containsExactlyEntriesOf(additionalProps); assertThat(m.getShowHotspotPayload().requestsCount()).isEqualTo(4); assertThat(m.getShowIssuePayload().requestsCount()).isEqualTo(3); assertThat(m.getHotspotPayload().openInBrowserCount()).isEqualTo(5); assertThat(m.getHotspotPayload().statusChangedCount()).isEqualTo(3); assertThat(m.getTaintVulnerabilitiesPayload().investigatedLocallyCount()).isEqualTo(6); assertThat(m.getTaintVulnerabilitiesPayload().investigatedRemotelyCount()).isEqualTo(7); assertRulesPayload(m); assertShareConnectedModePayload(m); assertMetadata(m); } private static void assertMetadata(TelemetryPayload m) { assertThat(m.getIdeVersion()).isEqualTo("Pycharm 3.2"); assertThat(m.getPlatform()).isEqualTo("platform"); assertThat(m.getArchitecture()).isEqualTo("architecture"); assertThat(m.getInstallTime()).hasToString("2017-11-10T12:01:14.984123123+02:00"); assertThat(m.os()).isEqualTo("Windows 10"); assertThat(m.jre()).isEqualTo("1.8.0"); assertThat(m.nodejs()).isEqualTo("10.5.2"); assertThat(m.daysOfUse()).isEqualTo(15); assertThat(m.daysSinceInstallation()).isEqualTo(4); assertThat(m.product()).isEqualTo("SLI"); assertThat(m.version()).isEqualTo("2.4"); } private static void assertShareConnectedModePayload(TelemetryPayload m) { assertThat(m.getShareConnectedModePayload().manualAddedBindingsCount()).isEqualTo(3); assertThat(m.getShareConnectedModePayload().importedAddedBindingsCount()).isEqualTo(2); assertThat(m.getShareConnectedModePayload().autoAddedBindingsCount()).isEqualTo(1); assertThat(m.getShareConnectedModePayload().exportedConnectedModeCount()).isEqualTo(4); } private static void assertRulesPayload(TelemetryPayload m) { assertThat(m.getTelemetryRulesPayload().defaultDisabled()).containsExactly("disabledRuleKey1", "disabledRuleKey2"); assertThat(m.getTelemetryRulesPayload().nonDefaultEnabled()).containsExactly("enabledRuleKey1", "enabledRuleKey2"); assertThat(m.getTelemetryRulesPayload().raisedIssues()).containsExactly("reportedRuleKey1", "reportedRuleKey2"); assertThat(m.getTelemetryRulesPayload().quickFixesApplied()).containsExactly("quickFixedRuleKey1", "quickFixedRuleKey2"); } private static TelemetryFixSuggestionPayload[] getTelemetryFixSuggestionPayloads() { var fixSuggestionPayload1 = new TelemetryFixSuggestionPayload("suggestionId1", 1, AiSuggestionSource.SONARCLOUD, List.of(new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.ACCEPTED, 0), new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.DECLINED, 1)), true); var fixSuggestionPayload2 = new TelemetryFixSuggestionPayload("suggestionId2", 2, AiSuggestionSource.SONARCLOUD, List.of(new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.ACCEPTED, null)), true); var fixSuggestionPayload3 = new TelemetryFixSuggestionPayload("suggestionId3", 3, AiSuggestionSource.SONARCLOUD, List.of(new TelemetryFixSuggestionResolvedPayload(null, null)), false); return new TelemetryFixSuggestionPayload[]{fixSuggestionPayload1, fixSuggestionPayload2, fixSuggestionPayload3}; } @Test void testMergeEmptyJson() { Map source = new LinkedHashMap<>(); Map target = new LinkedHashMap<>(); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{}"); } @Test void testMergeEmptyTargetJson() { Map source = new LinkedHashMap<>(); source.put("keyInSource", "valueInSource"); Map target = new LinkedHashMap<>(); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{\"keyInSource\":\"valueInSource\"}"); } @Test void testMergeEmptySourceJson() { Map source = new LinkedHashMap<>(); Map target = new LinkedHashMap<>(); target.put("keyInTarget", "valueInTarget"); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{\"keyInTarget\":\"valueInTarget\"}"); } @Test void testMergeJson() { Map source = new LinkedHashMap<>(); source.put("keyInSource", "valueInSource"); Map target = new LinkedHashMap<>(); target.put("keyInTarget", "valueInTarget"); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{\"keyInTarget\":\"valueInTarget\",\"keyInSource\":\"valueInSource\"}"); } @Test void testDeepMergeJson() { Map source = new LinkedHashMap<>(); Map sourceSub = new LinkedHashMap<>(); source.put("key", sourceSub); sourceSub.put("sub2", "sub2Value"); Map target = new LinkedHashMap<>(); Map targetSub = new LinkedHashMap<>(); target.put("key", targetSub); targetSub.put("sub1", "sub1Value"); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{\"key\":{\"sub1\":\"sub1Value\",\"sub2\":\"sub2Value\"}}"); } @Test void testMergeJsonDontOverrideExistingKey() { Map source = new LinkedHashMap<>(); source.put("key", "valueInSource"); Map target = new LinkedHashMap<>(); target.put("key", "valueInTarget"); var gson = new Gson(); var type = new TypeToken>() { }.getType(); var jsonSource = gson.toJsonTree(source, type).getAsJsonObject(); var jsonTarget = gson.toJsonTree(target, type).getAsJsonObject(); var merged = gson.toJson(TelemetryPayload.mergeObjects(jsonSource, jsonTarget)); assertThat(merged).isEqualTo("{\"key\":\"valueInTarget\"}"); } } ================================================ FILE: backend/telemetry/src/test/resources/response/gessie/GessieHttpClientTest/GessieRequest.json ================================================ { "metadata": { "event_id": "a36e25e8-5a92-4b5d-93b4-ba0045947b4c", "source": { "domain": "IntelliJ" }, "event_type": "Analytics.Test.TestEvent", "event_timestamp": "1761821877867", "event_version": "0" }, "event_payload": { "message": "Test event", "trigger": "test" } } ================================================ FILE: backend/telemetry/src/test/resources/response/gessie/GessieHttpClientTest/InvalidRequest.json ================================================ { "metadata": null, "event_payload": null } ================================================ FILE: buildSrc/README.md ================================================ # SonarLint Core: Build dependencies & logic This directory contains libraries, tools or scripts that are used by the build, e.g. as dependencies. These are not meant to be published but will be used / build for every build. ## Maven Shade Plugin: Transformer for Bndtools This is an extension to the [Maven Shade plug-in](https://maven.apache.org/plugins/maven-shade-plugin/) in order to work correctly on *MANIFEST.MF* files when used alongside the [Bndtools](https://github.com/bndtools/bnd/tree/master/maven-plugins/bnd-maven-plugin) on the same module. It provides a custom [Resource Transformer](https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html). The transformer `org.sonarsource.sonarlint.maven.shade.ext.ManifestBndTransformer` is used in order to exchange the *MANIFEST.MF* file on normal and sources JAR archives if the configuration offers a replacement. In the case of `sonarlint-java-client-osgi` this is used as the Bndtools are generating the *MANIFEST.MF* files and the OSGi content while the Maven Shade plug-in is used to pack the necessary dependencies or relocate others. To the console it logs all the dependencies it processing when shading / relocating in order to determine if shading / relocating is done on a normal or sources JAR archive. The log output is: > [maven-shade-ext-bnd-transformer : ManifestBndTransformer] Processing {Bundle-Name} / {Bundle-SymbolicName} per archive but both `Bundle-Name` and `Bundle-SymbolicName` can be null! It also logs once the Maven Shade plug-in comes to a close which *MANFIEST.MF* file will be used and moved into the JAR archive. ================================================ FILE: buildSrc/maven-shade-ext-bnd-transformer/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-parent 11.2-SNAPSHOT ../../pom.xml maven-shade-ext-bnd-transformer Maven Shade Plugin: Transformer for Bndtools Custom transformer used in combination with Bndtools org.apache.maven.plugins maven-shade-plugin ${version.shade.plugin} ================================================ FILE: buildSrc/maven-shade-ext-bnd-transformer/src/main/java/org/sonarsource/sonarlint/maven/shade/ext/ManifestBndTransformer.java ================================================ /* * Maven Shade Plugin: Transformer for Bndtools * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.maven.shade.ext; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import org.apache.maven.plugins.shade.relocation.Relocator; import org.apache.maven.plugins.shade.resource.ManifestResourceTransformer; import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer; /** * A custom Resource Transformer for the Maven Shade plug-in used in combination with the Bndtools for building OSGi bundles that are * consumed by the Eclipse IDE. The Maven plug-in and Bndtools are not optimized to work together but we use them in order to not require * different modules (one for shading, one for OSGi bundle, one for OSGi compliant source bundle) just to complete one operation. * * @author tobias.hahnen */ public class ManifestBndTransformer implements ReproducibleResourceTransformer { private static final String MANIFEST_ATTRIBUTE_BUNDLE_NAME = "Bundle-Name"; private static final String MANIFEST_ATTRIBUTE_BUNDLE_SYMBOLICNAME = "Bundle-SymbolicName"; // Configuration of the transformer in the pom.xml private String normalJarManifestPath; private String sourcesJarManifestPath; // Private fields used by the transformer private boolean isSourcesJarManifest = false; private Manifest normalJarManifest; private Manifest sourcesJarManifest; private Manifest manifest; private long time = Long.MIN_VALUE; /** This is used inside the pom.xml for configuring the transformer! */ public void setNormalJarManifestPath(String normalJarManifestPath) { this.normalJarManifestPath = normalJarManifestPath; } /** This is used inside the pom.xml for configuring the transformer! */ public void setSourcesJarManifestPath(String sourcesJarManifestPath) { this.sourcesJarManifestPath = sourcesJarManifestPath; } @Override public void processResource(String resource, InputStream is, List relocators, long time) throws IOException { // i) Load manifest object from the input stream and try to get the information if it manifest = new Manifest(is); var attributes = manifest.getMainAttributes(); var bundleName = attributes.getValue(MANIFEST_ATTRIBUTE_BUNDLE_NAME); var bundleSymbolicName = attributes.getValue(MANIFEST_ATTRIBUTE_BUNDLE_SYMBOLICNAME); info("Processing " + bundleName + " / " + bundleSymbolicName); isSourcesJarManifest = isSourcesJarManifest || (bundleName != null && (bundleName.endsWith(" Source") || bundleName.endsWith(" Sources"))); isSourcesJarManifest = isSourcesJarManifest || (bundleSymbolicName != null && (bundleSymbolicName.endsWith(".source") || bundleSymbolicName.endsWith(".sources"))); // ii) Set the time that is later used when saving the JAR archive entry! if (time > this.time) { this.time = time; } } @Override public void modifyOutputStream(JarOutputStream jos) throws IOException { // i) the first time this is called we load the normal / sources JAR manifest file tryLoadNormalJarManifest(); tryLoadSourcesJarManifest(); // ii) Setup the manifest object that should be written in the end if (isSourcesJarManifest && sourcesJarManifest != null) { manifest = sourcesJarManifest; info("Exchanging META-INF/MANIFEST.MF (sources) with: " + sourcesJarManifestPath); } else if (!isSourcesJarManifest && normalJarManifest != null) { manifest = normalJarManifest; info("Exchanging META-INF/MANIFEST.MF (normal) with: " + normalJarManifestPath); } else { manifest = manifest != null ? manifest : new Manifest(); info("Not exchanging META-INF/MANIFEST.MF"); } // iii) Propose output stream content for the META-INF/MANIFEST.MF var jarEntry = new JarEntry(JarFile.MANIFEST_NAME); jarEntry.setTime(time); jos.putNextEntry(jarEntry); manifest.write(jos); } /** Try to load the Manifest with attributes (normal) if not already loaded */ private void tryLoadNormalJarManifest() throws IOException { if (normalJarManifestPath != null && normalJarManifest == null) { var manifestFile = new File(normalJarManifestPath); if (manifestFile.exists()) { normalJarManifest = new Manifest(new FileInputStream(manifestFile)); } else { error("Manifest with attributes (normal) does not exist at: " + normalJarManifestPath); } } } /** Try to load the Manifest with attributes (sources) if not already loaded */ private void tryLoadSourcesJarManifest() throws IOException { if (sourcesJarManifestPath != null && sourcesJarManifest == null) { var manifestFile = new File(sourcesJarManifestPath); if (manifestFile.exists()) { sourcesJarManifest = new Manifest(new FileInputStream(manifestFile)); } else { error("Manifest with attributes (sources) does not exist at: " + sourcesJarManifestPath); } } } /** Log info to console */ private static void info(String message) { System.err.println("[maven-shade-ext-bnd-transformer : ManifestBndTransformer] " + message); } /** Log error to console */ private static void error(String message) { System.err.println("[maven-shade-ext-bnd-transformer : ManifestBndTransformer] " + message); } /** * Implementation is copied from the official implementation of * {@link ManifestResourceTransformer#processResource(String, InputStream, List)} */ @Override public void processResource(String resource, InputStream is, List relocators) throws IOException { processResource(resource, is, relocators, 0); } /** Implementation is copied from the official implementation of {@link ManifestResourceTransformer#canTransformResource(String)} */ @Override public boolean canTransformResource(String resource) { return JarFile.MANIFEST_NAME.equalsIgnoreCase(resource); } /** * Implementation is copied from the official implementation of {@link ManifestResourceTransformer#hasTransformedResource()} */ @Override public boolean hasTransformedResource() { return true; } } ================================================ FILE: client/java-client-dependencies/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-client-parent 11.2-SNAPSHOT ../pom.xml sonarlint-java-client-dependencies SonarLint Core - Java Client dependencies Dependencies used by the Java Clients, shaded and relocated together com.google.code.gson gson ${gson.version} org.eclipse.lsp4j org.eclipse.lsp4j.jsonrpc ${lsp4j.version} org.eclipse.jgit org.eclipse.jgit ${jgit6.version} org.slf4j slf4j-api ${slf4j.version} io.sentry sentry ${sentry.version} org.apache.maven.plugins maven-jar-plugin empty-javadoc-jar package jar javadoc ${basedir}/javadoc org.apache.maven.plugins maven-shade-plugin shade package shade true false true com.google.code.gson:gson org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc org.eclipse.jgit:org.eclipse.jgit org.slf4j:slf4j-api io.sentry:sentry org.sonarsource.sonarlint.shaded. com.google.gson.** org.eclipse.lsp4j.** org.eclipse.jgit.** org.slf4j.** io.sentry.** *:* module-info.class about.html META-INF/*.SF META-INF/*.DSA META-INF/*.RSA META-INF/LICENSE* META-INF/NOTICE* OSGI-INF/ LICENSE* NOTICE* *.proto ================================================ FILE: client/java-client-osgi/java-client-osgi-sources.bnd ================================================ # Include BND settings used for normal / sources JAR archives -include: shared.bnd # Manifest entries to configure the OSGi attributes for the sources JAR archive Bundle-Name: ${project.name} Source Bundle-Description: ${project.name} Source Bundle-SymbolicName: ${project.groupId}.${project.artifactId}.source Eclipse-SourceBundle: ${project.groupId}.${project.artifactId};version="${parsedVersion.osgiVersion}";roots:="." Export-Package: !* Import-Package: !* ================================================ FILE: client/java-client-osgi/java-client-osgi.bnd ================================================ # Include BND settings used for normal / sources JAR archives -include: shared.bnd # Manifest entries to configure the OSGi attributes for the normal JAR archive Bundle-SymbolicName: ${project.groupId}.${project.artifactId} Export-Package: org.sonarsource.sonarlint.core.client.utils.*;version="${project.version}",\ org.sonarsource.sonarlint.core.rpc.client.*;version="${project.version}",\ org.sonarsource.sonarlint.core.rpc.protocol.*;version="${project.version}",\ org.sonarsource.sonarlint.shaded.com.google.gson.*;version="${gson.version}",\ org.sonarsource.sonarlint.shaded.org.eclipse.lsp4j.jsonrpc.*;version="${lsp4j.version}",\ org.sonarsource.sonarlint.shaded.org.eclipse.jgit.*;version="${jgit6.version}",\ org.sonarsource.sonarlint.shaded.org.slf4j.*;version="${slf4j.version}",\ org.sonarsource.sonarlint.shaded.io.sentry.*;version="${sentry.version}", Import-Package: javax.annotation.*;resolution:=optional, ================================================ FILE: client/java-client-osgi/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-client-parent 11.2-SNAPSHOT ../pom.xml sonarlint-java-client-osgi SonarLint Core - Java Client OSGi Common SonarLint features bundled for OSGi ${project.groupId} sonarlint-java-client-dependencies ${project.version} ${project.groupId} sonarlint-java-client-utils ${project.version} ${project.groupId} sonarlint-rpc-java-client ${project.version} ${project.groupId} sonarlint-rpc-protocol ${project.version} org.apache.maven.plugins maven-jar-plugin empty-javadoc-jar package jar javadoc ${basedir}/javadoc org.codehaus.mojo build-helper-maven-plugin parse-version parse-version biz.aQute.bnd bnd-maven-plugin true prepare-normal-MANIFEST.MF bnd-process ${project.basedir}/java-client-osgi.bnd ${project.basedir}/target/normal-MANIFEST.MF prepare-sources-MANIFEST.MF bnd-process ${project.basedir}/java-client-osgi-sources.bnd ${project.basedir}/target/sources-MANIFEST.MF org.apache.maven.plugins maven-shade-plugin ${project.groupId} maven-shade-ext-bnd-transformer ${project.version} shade package shade true true true ${project.basedir}/target/normal-MANIFEST.MF ${project.basedir}/target/sources-MANIFEST.MF ${project.groupId}:sonarlint-java-client-dependencies ${project.groupId}:sonarlint-java-client-utils ${project.groupId}:sonarlint-rpc-java-client ${project.groupId}:sonarlint-rpc-protocol org.sonarsource.sonarlint.shaded. com.google.gson.** org.eclipse.lsp4j.** org.eclipse.jgit.** org.slf4j.** io.sentry.** *:* module-info.class logback-shared.xml sl_core_version.txt META-INF/*.SF META-INF/*.DSA META-INF/*.RSA META-INF/LICENSE* META-INF/NOTICE* LICENSE* NOTICE* *.proto ${project.groupId}:* ** ================================================ FILE: client/java-client-osgi/shared.bnd ================================================ # Manifest entries to configure the OSGi attributes for the normal / sources JAR archive Bundle-ManifestVersion: 2 Bundle-Version: ${parsedVersion.osgiVersion} # BND configuration to tweak generation of Manifest entries for the normal / sources JAR archive -removeheaders: Bnd-LastModified,Bundle-Developers,Bundle-DocURL,Bundle-SCM,Include-Resource,Private-Package -noimportjava: true # a new version of apache commons-io library has a new META-INF/versions/9/module-info.class file and that does not conform OSGi -fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning ================================================ FILE: client/java-client-utils/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-client-parent 11.2-SNAPSHOT ../pom.xml sonarlint-java-client-utils SonarLint Core - Java Client Utils Utility classes for Java clients com.google.code.findbugs jsr305 provided org.eclipse.jgit org.eclipse.jgit ${jgit6.version} provided true ${project.groupId} sonarlint-rpc-protocol ${project.version} org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/CleanCodeAttribute.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum CleanCodeAttribute { CONVENTIONAL("Not conventional", CleanCodeAttributeCategory.CONSISTENT), FORMATTED("Not formatted", CleanCodeAttributeCategory.CONSISTENT), IDENTIFIABLE("Not identifiable", CleanCodeAttributeCategory.CONSISTENT), CLEAR("Not clear", CleanCodeAttributeCategory.INTENTIONAL), COMPLETE("Not complete", CleanCodeAttributeCategory.INTENTIONAL), EFFICIENT("Not efficient", CleanCodeAttributeCategory.INTENTIONAL), LOGICAL("Not logical", CleanCodeAttributeCategory.INTENTIONAL), DISTINCT("Not distinct", CleanCodeAttributeCategory.ADAPTABLE), FOCUSED("Not focused", CleanCodeAttributeCategory.ADAPTABLE), MODULAR("Not modular", CleanCodeAttributeCategory.ADAPTABLE), TESTED("Not tested", CleanCodeAttributeCategory.ADAPTABLE), LAWFUL("Not lawful", CleanCodeAttributeCategory.RESPONSIBLE), RESPECTFUL("Not respectful", CleanCodeAttributeCategory.RESPONSIBLE), TRUSTWORTHY("Not trustworthy", CleanCodeAttributeCategory.RESPONSIBLE); private final String label; private final CleanCodeAttributeCategory category; CleanCodeAttribute(String label, CleanCodeAttributeCategory category) { this.label = label; this.category = category; } public String getLabel() { return label; } public CleanCodeAttributeCategory getCategory() { return category; } public static CleanCodeAttribute fromDto(org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute rpcEnum) { switch (rpcEnum) { case CONVENTIONAL: return CONVENTIONAL; case FORMATTED: return FORMATTED; case IDENTIFIABLE: return IDENTIFIABLE; case CLEAR: return CLEAR; case COMPLETE: return COMPLETE; case EFFICIENT: return EFFICIENT; case LOGICAL: return LOGICAL; case DISTINCT: return DISTINCT; case FOCUSED: return FOCUSED; case MODULAR: return MODULAR; case TESTED: return TESTED; case LAWFUL: return LAWFUL; case RESPECTFUL: return RESPECTFUL; case TRUSTWORTHY: return TRUSTWORTHY; default: throw new IllegalArgumentException("Unknown attribute: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/CleanCodeAttributeCategory.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum CleanCodeAttributeCategory { ADAPTABLE("Adaptability"), CONSISTENT("Consistency"), INTENTIONAL("Intentionality"), RESPONSIBLE("Responsibility"); private final String label; CleanCodeAttributeCategory(String label) { this.label = label; } public String getLabel() { return label; } public static CleanCodeAttributeCategory fromDto(org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttributeCategory rpcEnum) { switch (rpcEnum) { case ADAPTABLE: return ADAPTABLE; case CONSISTENT: return CONSISTENT; case INTENTIONAL: return INTENTIONAL; case RESPONSIBLE: return RESPONSIBLE; default: throw new IllegalArgumentException("Unknown category: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/ClientFileExclusions.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.io.File; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.Predicate; /** * Exclusions configured on client side */ public class ClientFileExclusions implements Predicate { private static final String SYNTAX = "glob"; private final List matchers; private final Set directoryExclusions; private final Set fileExclusions; public ClientFileExclusions(Set fileExclusions, Set directoryExclusions, Set globPatterns) { this.fileExclusions = fileExclusions; this.directoryExclusions = directoryExclusions; this.matchers = parseGlobPatterns(globPatterns); } private static List parseGlobPatterns(Set globPatterns) { var fs = FileSystems.getDefault(); List parsedMatchers = new ArrayList<>(globPatterns.size()); for (String pattern : globPatterns) { try { parsedMatchers.add(fs.getPathMatcher(SYNTAX + ":" + pattern)); } catch (Exception e) { // ignore invalid patterns, simply skip them } } return parsedMatchers; } public boolean test(Path path) { return testFileExclusions(path) || testDirectoryExclusions(path) || testGlob(path); } private boolean testGlob(Path path) { return matchers.stream().anyMatch(matcher -> matcher.matches(path)); } private boolean testFileExclusions(Path path) { return hasOsIndependentExclusion(fileExclusions, path); } private boolean testDirectoryExclusions(Path path) { var p = path; while (p != null) { if (hasOsIndependentExclusion(directoryExclusions, p)) { return true; } p = p.getParent(); } return false; } private static boolean hasOsIndependentExclusion(Set exclusions, Path path) { var pathStr = path.toString(); return exclusions.contains(pathStr) || exclusions.contains(pathStr.replace(File.separatorChar, '/')) || exclusions.contains(pathStr.replace(File.separatorChar, '\\')); } @Override public boolean test(String string) { return test(Paths.get(string)); } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/ClientLogOutput.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.io.PrintWriter; import java.io.StringWriter; /** * Allow to redirect SonarLint logs to a custom output on client side */ public interface ClientLogOutput { void log(String formattedMessage, Level level); enum Level { ERROR, WARN, INFO, DEBUG, TRACE } static String stackTraceToString(Throwable t) { var stringWriter = new StringWriter(); var printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); return stringWriter.toString(); } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/DateUtils.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; public class DateUtils { private DateUtils() { // utility class, forbidden constructor } public static String toAge(long time) { var creation = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()); var now = LocalDateTime.now(); var years = ChronoUnit.YEARS.between(creation, now); if (years > 0) { return pluralize(years, "year"); } var months = ChronoUnit.MONTHS.between(creation, now); if (months > 0) { return pluralize(months, "month"); } var days = ChronoUnit.DAYS.between(creation, now); if (days > 0) { return pluralize(days, "day"); } var hours = ChronoUnit.HOURS.between(creation, now); if (hours > 0) { return pluralize(hours, "hour"); } var minutes = ChronoUnit.MINUTES.between(creation, now); if (minutes > 0) { return pluralize(minutes, "minute"); } return "few seconds ago"; } private static String pluralize(long strictlyPositiveCount, String singular) { return pluralize(strictlyPositiveCount, singular, singular + "s"); } private static String pluralize(long strictlyPositiveCount, String singular, String plural) { if (strictlyPositiveCount == 1) { return "1 " + singular + " ago"; } return strictlyPositiveCount + " " + plural + " ago"; } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/DependencyRiskTransitionStatus.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskDto; public enum DependencyRiskTransitionStatus { REOPEN("Open", "This finding has not yet been reviewed."), CONFIRM("Confirmed", "This finding has been reviewed and the risk is valid."), ACCEPT("Accepted", "This finding is valid, but it may not be fixed for a while."), SAFE("Safe", "This finding does not pose a risk. No fix is needed."), FIXED("Fixed", "This finding has been fixed."); private final String title; private final String description; DependencyRiskTransitionStatus(String title, String description) { this.title = title; this.description = description; } public String getTitle() { return title; } public String getDescription() { return description; } public static DependencyRiskTransitionStatus fromDto(DependencyRiskDto.Transition status) { switch (status) { case REOPEN: return REOPEN; case CONFIRM: return CONFIRM; case ACCEPT: return ACCEPT; case SAFE: return SAFE; case FIXED: return FIXED; default: throw new IllegalArgumentException("Unknown status: " + status); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/GitUtils.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryBuilder; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalkUtils; import org.eclipse.jgit.revwalk.filter.RevFilter; import static java.util.Comparator.naturalOrder; public class GitUtils { private GitUtils() { // util class } @CheckForNull public static Repository getRepositoryForDir(Path projectDir, ClientLogOutput clientLogOutput) { try { var builder = new RepositoryBuilder() .findGitDir(projectDir.toFile()) .setMustExist(true); if (builder.getGitDir() == null) { clientLogOutput.log("Not inside a Git work tree: " + projectDir, ClientLogOutput.Level.DEBUG); return null; } return builder.build(); } catch (IOException e) { clientLogOutput.log("Couldn't access repository for path " + projectDir, ClientLogOutput.Level.ERROR); clientLogOutput.log(ClientLogOutput.stackTraceToString(e), ClientLogOutput.Level.ERROR); } return null; } @CheckForNull public static String electBestMatchingServerBranchForCurrentHead(Repository repo, Set serverCandidateNames, @Nullable String serverMainBranch, ClientLogOutput clientLogOutput) { try { String currentBranch = repo.getBranch(); if (currentBranch != null && serverCandidateNames.contains(currentBranch)) { return currentBranch; } var head = repo.exactRef(Constants.HEAD); if (head == null) { // Not sure if this is possible to not have a HEAD, but just in case return null; } Map> branchesPerDistance = new HashMap<>(); for (String serverBranchName : serverCandidateNames) { var shortBranchName = Repository.shortenRefName(serverBranchName); var localFullBranchName = Constants.R_HEADS + shortBranchName; var branchRef = repo.exactRef(localFullBranchName); if (branchRef == null) { continue; } int distance = distance(repo, head, branchRef); branchesPerDistance.computeIfAbsent(distance, d -> new HashSet<>()).add(serverBranchName); } if (branchesPerDistance.isEmpty()) { return null; } int minDistance = branchesPerDistance.keySet().stream().min(naturalOrder()).get(); var bestCandidates = branchesPerDistance.get(minDistance); if (serverMainBranch != null && bestCandidates.contains(serverMainBranch)) { // Favor the main branch when there are multiple candidates with the same distance return serverMainBranch; } return bestCandidates.iterator().next(); } catch (IOException e) { clientLogOutput.log("Couldn't find best matching branch", ClientLogOutput.Level.ERROR); clientLogOutput.log(ClientLogOutput.stackTraceToString(e), ClientLogOutput.Level.ERROR); return null; } } private static int distance(Repository repository, Ref from, Ref to) throws IOException { try (var walk = new RevWalk(repository)) { var fromCommit = walk.parseCommit(from.getObjectId()); var toCommit = walk.parseCommit(to.getObjectId()); walk.setRevFilter(RevFilter.MERGE_BASE); walk.markStart(fromCommit); walk.markStart(toCommit); var mergeBase = walk.next(); walk.reset(); walk.setRevFilter(RevFilter.ALL); int aheadCount = RevWalkUtils.count(walk, fromCommit, mergeBase); int behindCount = RevWalkUtils.count(walk, toCommit, mergeBase); return aheadCount + behindCount; } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/HotspotStatus.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum HotspotStatus { // order is important here, it will be applied in the UI TO_REVIEW("To Review", "This Security Hotspot needs to be reviewed to assess whether the code poses a risk."), ACKNOWLEDGED("Acknowledged", "The code has been reviewed and does pose a risk. A fix is required."), FIXED("Fixed", "The code has been modified to follow recommended secure coding practices."), SAFE("Safe", "The code has been reviewed and does not pose a risk. It does not need to be modified."); private final String title; private final String description; HotspotStatus(String title, String description) { this.title = title; this.description = description; } public String getTitle() { return title; } public String getDescription() { return description; } public static HotspotStatus fromDto(org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus rpcEnum) { switch (rpcEnum) { case TO_REVIEW: return TO_REVIEW; case ACKNOWLEDGED: return ACKNOWLEDGED; case FIXED: return FIXED; case SAFE: return SAFE; default: throw new IllegalArgumentException("Unknown status: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/ImpactSeverity.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum ImpactSeverity { INFO("Info"), LOW("Low"), MEDIUM("Medium"), HIGH("High"), BLOCKER("Blocker"); private final String label; ImpactSeverity(String label) { this.label = label; } public String getLabel() { return label; } public static ImpactSeverity fromDto(org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity rpcEnum) { switch (rpcEnum) { case INFO: return INFO; case LOW: return LOW; case MEDIUM: return MEDIUM; case HIGH: return HIGH; case BLOCKER: return BLOCKER; default: throw new IllegalArgumentException("Unknown severity: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/IssueResolutionStatus.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; public enum IssueResolutionStatus { ACCEPT("Accepted", "The issue is valid but will not be fixed now. It represents accepted technical debt."), WONT_FIX("Won't Fix", "The issue is valid but does not need fixing. It represents accepted technical debt."), FALSE_POSITIVE("False Positive", "The issue is raised unexpectedly on code that should not trigger an issue."); private final String title; private final String description; IssueResolutionStatus(String title, String description) { this.title = title; this.description = description; } public String getTitle() { return title; } public String getDescription() { return description; } public static IssueResolutionStatus fromDto(ResolutionStatus status) { switch (status) { case ACCEPT: return ACCEPT; case WONT_FIX: return WONT_FIX; case FALSE_POSITIVE: return FALSE_POSITIVE; default: throw new IllegalArgumentException("Unknown status: " + status); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/Language.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum Language { ABAP("ABAP"), APEX("Apex"), C("C"), CPP("C++"), CS("C#"), CSS("CSS"), OBJC("Objective-C"), COBOL("COBOL"), HTML("HTML"), IPYTHON("IPython Notebooks"), JAVA("Java"), JCL("JCL"), JS("JavaScript"), KOTLIN("Kotlin"), PHP("PHP"), PLI("PL/I"), PLSQL("PL/SQL"), PYTHON("Python"), RPG("RPG"), RUBY("Ruby"), SCALA("Scala"), SECRETS("Secrets"), TEXT("Text"), SWIFT("Swift"), TSQL("T-SQL"), TS("TypeScript"), JSP("JSP"), VBNET("VB.NET"), XML("XML"), YAML("YAML"), JSON("JSON"), GO("Go"), CLOUDFORMATION("CloudFormation"), DOCKER("Docker"), KUBERNETES("Kubernetes"), TERRAFORM("Terraform"), AZURERESOURCEMANAGER("AzureResourceManager"), ANSIBLE("Ansible"), GITHUBACTIONS("GitHub Actions"); private final String label; Language(String label) { this.label = label; } public String getLabel() { return label; } public static Language fromDto(org.sonarsource.sonarlint.core.rpc.protocol.common.Language rpcEnum) { switch (rpcEnum) { case ABAP: return ABAP; case APEX: return APEX; case C: return C; case CPP: return CPP; case CS: return CS; case CSS: return CSS; case OBJC: return OBJC; case COBOL: return COBOL; case GITHUBACTIONS: return GITHUBACTIONS; case HTML: return HTML; case IPYTHON: return IPYTHON; case JAVA: return JAVA; case JCL: return JCL; case JS: return JS; case KOTLIN: return KOTLIN; case PHP: return PHP; case PLI: return PLI; case PLSQL: return PLSQL; case PYTHON: return PYTHON; case RPG: return RPG; case RUBY: return RUBY; case SCALA: return SCALA; case SECRETS: return SECRETS; case TEXT: return TEXT; case SWIFT: return SWIFT; case TSQL: return TSQL; case TS: return TS; case JSP: return JSP; case VBNET: return VBNET; case XML: return XML; case YAML: return YAML; case JSON: return JSON; case GO: return GO; case CLOUDFORMATION: return CLOUDFORMATION; case DOCKER: return DOCKER; case KUBERNETES: return KUBERNETES; case TERRAFORM: return TERRAFORM; case AZURERESOURCEMANAGER: return AZURERESOURCEMANAGER; case ANSIBLE: return ANSIBLE; default: throw new IllegalArgumentException("Unknown language: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/SoftwareQuality.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; public enum SoftwareQuality { MAINTAINABILITY("Maintainability"), RELIABILITY("Reliability"), SECURITY("Security"); private final String label; SoftwareQuality(String label) { this.label = label; } public String getLabel() { return label; } public static SoftwareQuality fromDto(org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality rpcEnum) { switch (rpcEnum) { case MAINTAINABILITY: return MAINTAINABILITY; case RELIABILITY: return RELIABILITY; case SECURITY: return SECURITY; default: throw new IllegalArgumentException("Unknown quality: " + rpcEnum); } } } ================================================ FILE: client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/package-info.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.client.utils; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/CleanCodeAttributeCategoryTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CleanCodeAttributeCategoryTests { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttributeCategory.values()) { var converted = CleanCodeAttributeCategory.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/CleanCodeAttributeTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CleanCodeAttributeTests { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute.values()) { var converted = CleanCodeAttribute.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/ClientFileExclusionsTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.io.File; import java.util.Collections; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import static org.assertj.core.api.Assertions.assertThat; class ClientFileExclusionsTests { ClientFileExclusions underTest; @BeforeEach void before() { Set glob = Collections.singleton("**/*.js"); // Setup file exclusions with both separator styles Set files = Set.of( new File("dir/file.java").getAbsolutePath(), "dir/file-with-slash.java", "other\\file-with-backslash.java" ); // Setup directory exclusions with both separator styles Set dirs = Set.of( "src", "excluded/dir", "another\\excluded\\dir" ); underTest = new ClientFileExclusions(files, dirs, glob); } @Test void should_exclude_with_glob_relative_path() { assertThat(underTest.test(new File("dir2/file.js").getAbsolutePath())).isTrue(); assertThat(underTest.test(new File("dir2/file.java").getAbsolutePath())).isFalse(); } @Test void should_exclude_with_glob_absolute_path() { assertThat(underTest.test(new File("/absolute/dir/file.js").getAbsolutePath())).isTrue(); assertThat(underTest.test(new File("/absolute/dir/file.java").getAbsolutePath())).isFalse(); } @Test void should_exclude_with_file() { assertThat(underTest.test(new File("dir/file2.java").getAbsolutePath())).isFalse(); assertThat(underTest.test(new File("dir/file.java").getAbsolutePath())).isTrue(); } @Test void should_exclude_with_dir() { assertThat(underTest.test(new File("dir/class2.java").getAbsolutePath())).isFalse(); assertThat(underTest.test("src/class.java")).isTrue(); } @Test void should_handle_file_exclusions_with_different_separators() { assertThat(underTest.test("dir/file-with-slash.java")).isTrue(); assertThat(underTest.test("other/file-with-backslash.java")).isTrue(); assertThat(underTest.test("different/dir/file-with-slash.java")).isFalse(); assertThat(underTest.test("other2/file-with-backslash.java")).isFalse(); } @Test void should_handle_directory_exclusions_with_different_separators() { assertThat(underTest.test("excluded/dir/some-file.java")).isTrue(); assertThat(underTest.test("another/excluded/dir/some-file.java")).isTrue(); assertThat(underTest.test("different/excluded/some-file.java")).isFalse(); assertThat(underTest.test("another2\\excluded\\dir\\some-file.java")).isFalse(); } @EnabledOnOs(OS.WINDOWS) @Test void testFileExclusionsWithBackslashes() { assertThat(underTest.test("dir\\file-with-slash.java")).isTrue(); } @EnabledOnOs(OS.WINDOWS) @Test void testDirectoryExclusionsWithBackslashes() { assertThat(underTest.test("excluded\\dir\\some-file.java")).isTrue(); } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/DateUtilsTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.time.LocalDateTime; import java.time.ZoneId; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class DateUtilsTests { @Test void testAge() { assertThat(DateUtils.toAge(System.currentTimeMillis() - 100)).isEqualTo("few seconds ago"); assertThat(DateUtils.toAge(System.currentTimeMillis() - 65_000)).isEqualTo("1 minute ago"); assertThat(DateUtils.toAge(System.currentTimeMillis() - 3_600_000 - 100_000)).isEqualTo("1 hour ago"); assertThat(DateUtils.toAge(System.currentTimeMillis() - 2 * 3_600_000 - 100_000)).isEqualTo("2 hours ago"); assertThat(DateUtils.toAge(System.currentTimeMillis() - 24 * 3_600_000 - 100_000)).isEqualTo("1 day ago"); assertThat(DateUtils.toAge(LocalDateTime.now().minusMonths(5) .atZone(ZoneId.systemDefault()) .toInstant() .toEpochMilli())).isEqualTo("5 months ago"); assertThat(DateUtils.toAge(LocalDateTime.now().minusMonths(15) .atZone(ZoneId.systemDefault()) .toInstant() .toEpochMilli())).isEqualTo("1 year ago"); } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/DependencyRiskTransitionStatusTest.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskDto; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; class DependencyRiskTransitionStatusTest { @Test void should_convert_all_enum_values() { for (var rpcEnum : DependencyRiskDto.Transition.values()) { var converted = DependencyRiskTransitionStatus.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } @Test void should_get_title() { assertThat(DependencyRiskTransitionStatus.SAFE.getTitle()).isEqualTo("Safe"); } @Test void should_get_description() { assertThat(DependencyRiskTransitionStatus.FIXED.getDescription()) .isEqualTo("This finding has been fixed."); } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/GitUtilsTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static java.lang.String.format; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class GitUtilsTests { private final ClientLogOutput fakeClientLogger = (m, l) -> { }; @Test void noGitRepoShouldBeNull(@TempDir File projectDir) throws IOException { javaUnzip("no-git-repo.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "no-git-repo"); Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger); assertThat(repo).isNull(); } @Test void gitRepoShouldBeNotNull(@TempDir File projectDir) throws IOException { javaUnzip("dummy-git.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "dummy-git"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("foo", "bar", "master"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "master", fakeClientLogger); assertThat(branch).isEqualTo("master"); } } @Test void shouldElectAnalyzedBranch(@TempDir File projectDir) throws IOException { javaUnzip("analyzed-branch.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "analyzed-branch"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("foo", "closest_branch", "master"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "master", fakeClientLogger); assertThat(branch).isEqualTo("closest_branch"); } } @Test void shouldReturnNullIfNonePresentInLocalGit(@TempDir File projectDir) throws IOException { javaUnzip("analyzed-branch.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "analyzed-branch"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("unknown1", "unknown2", "unknown3"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "master", fakeClientLogger); assertThat(branch).isNull(); } } @Test void shouldElectClosestBranch(@TempDir File projectDir) throws IOException { javaUnzip("closest-branch.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "closest-branch"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("foo", "closest_branch", "master"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "master", fakeClientLogger); assertThat(branch).isEqualTo("closest_branch"); } } @Test void shouldElectClosestBranch_even_if_no_main_branch(@TempDir File projectDir) throws IOException { javaUnzip("closest-branch.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "closest-branch"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("foo", "closest_branch", "master"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, null, fakeClientLogger); assertThat(branch).isEqualTo("closest_branch"); } } @Test void shouldElectMainBranchForNonAnalyzedChildBranch(@TempDir File projectDir) throws IOException { javaUnzip("child-from-non-analyzed.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "child-from-non-analyzed"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("foo", "branch_to_analyze", "master"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "master", fakeClientLogger); assertThat(branch).isEqualTo("master"); } } @Test void shouldReturnNullOnException() throws IOException { Repository repo = mock(Repository.class); RefDatabase db = mock(RefDatabase.class); when(repo.getRefDatabase()).thenReturn(db); when(db.exactRef(anyString())).thenThrow(new IOException()); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, Set.of("foo", "bar", "master"), "master", fakeClientLogger); assertThat(branch).isNull(); } @Test void shouldFavorCurrentBranchIfMultipleCandidates(@TempDir File projectDir) throws IOException { // Both main and same-as-master branches are pointing to HEAD, but same-as-master is the currently checked out branch javaUnzip("two-branches-for-head.zip", projectDir); Path path = Paths.get(projectDir.getPath(), "two-branches-for-head"); try (Repository repo = GitUtils.getRepositoryForDir(path, fakeClientLogger)) { Set serverCandidateNames = Set.of("main", "same-as-master", "another"); String branch = GitUtils.electBestMatchingServerBranchForCurrentHead(repo, serverCandidateNames, "main", fakeClientLogger); assertThat(branch).isEqualTo("same-as-master"); } } public void javaUnzip(String zipFileName, File toDir) throws IOException { File testRepos = new File("src/test/test-repos"); File zipFile = new File(testRepos, zipFileName); javaUnzip(zipFile, toDir); } private static void javaUnzip(File zip, File toDir) { try { try (ZipFile zipFile = new ZipFile(zip)) { Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); File to = new File(toDir, entry.getName()); if (entry.isDirectory()) { forceMkdir(to); } else { File parent = to.getParentFile(); forceMkdir(parent); Files.copy(zipFile.getInputStream(entry), to.toPath()); } } } } catch (Exception e) { throw new IllegalStateException(format("Fail to unzip %s to %s", zip, toDir), e); } } private static void forceMkdir(final File directory) throws IOException { if ((directory != null) && (!directory.mkdirs() && !directory.isDirectory())) { throw new IOException("Cannot create directory '" + directory + "'."); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/HotspotStatusTest.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class HotspotStatusTest { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus.values()) { var converted = HotspotStatus.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/ImpactSeverityTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class ImpactSeverityTests { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity.values()) { var converted = ImpactSeverity.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/IssueResolutionStatusTest.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import org.sonarsource.sonarlint.core.rpc.protocol.backend.issue.ResolutionStatus; import static org.junit.jupiter.api.Assertions.assertEquals; class IssueResolutionStatusTest { @Test void should_convert_all_enum_values() { for (var rpcEnum : ResolutionStatus.values()) { var converted = IssueResolutionStatus.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/LanguageTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class LanguageTests { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.common.Language.values()) { var converted = Language.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/java-client-utils/src/test/java/org/sonarsource/sonarlint/core/client/utils/SoftwareQualityTests.java ================================================ /* * SonarLint Core - Java Client Utils * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.client.utils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class SoftwareQualityTests { @Test void should_convert_all_enum_values() { for (var rpcEnum : org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality.values()) { var converted = SoftwareQuality.fromDto(rpcEnum); assertEquals(rpcEnum.name(), converted.name()); } } } ================================================ FILE: client/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-parent 11.2-SNAPSHOT ../pom.xml sonarlint-client-parent pom SonarLint Core - Client 11 java-client-dependencies java-client-utils java-client-osgi rpc-java-client ================================================ FILE: client/rpc-java-client/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-client-parent 11.2-SNAPSHOT ../pom.xml sonarlint-rpc-java-client SonarLint Core - RPC Java Client Java client for SonarLint RPC com.google.code.findbugs jsr305 provided org.eclipse.lsp4j org.eclipse.lsp4j.jsonrpc ${lsp4j.version} ${project.groupId} sonarlint-rpc-protocol ${project.version} io.sentry sentry ${sentry.version} org.junit.jupiter junit-jupiter-engine test org.assertj assertj-core test org.mockito mockito-core test org.apache.commons commons-lang3 test org.apache.maven.plugins maven-source-plugin attach-sources package jar-no-fork true true ${project.name} Source ${project.groupId}.${project.artifactId}.source ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/ClientJsonRpcLauncher.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.sonarsource.sonarlint.core.rpc.protocol.RpcErrorHandler; import org.sonarsource.sonarlint.core.rpc.protocol.SingleThreadedMessageConsumer; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintLauncherBuilder; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; public class ClientJsonRpcLauncher implements Closeable { private final SonarLintRpcServer serverProxy; private final Future future; private final ExecutorService messageReaderExecutor; private final ExecutorService messageWriterExecutor; private final ExecutorService requestAndNotificationsSequentialExecutor; private final ExecutorService requestsExecutor; public ClientJsonRpcLauncher(InputStream in, OutputStream out, SonarLintRpcClientDelegate clientDelegate) { messageReaderExecutor = Executors.newCachedThreadPool(r -> { var t = new Thread(r); t.setName("Client message reader"); return t; }); messageWriterExecutor = Executors.newCachedThreadPool(r -> { var t = new Thread(r); t.setName("Client message writer"); return t; }); this.requestAndNotificationsSequentialExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "SonarLint Client RPC sequential executor")); this.requestsExecutor = Executors.newCachedThreadPool(r -> new Thread(r, "SonarLint Client RPC request executor")); var client = new SonarLintRpcClientImpl(clientDelegate, requestsExecutor, requestAndNotificationsSequentialExecutor); var clientLauncher = new SonarLintLauncherBuilder() .setLocalService(client) .setRemoteInterface(SonarLintRpcServer.class) .setInput(in) .setOutput(out) .setExecutorService(messageReaderExecutor) .wrapMessages(m -> new SingleThreadedMessageConsumer(m, messageWriterExecutor, ex -> client.logClientSideError("Error consuming RPC message", ex))) .traceMessages(getMessageTracer()) .setExceptionHandler(RpcErrorHandler::handleError) .create(); this.serverProxy = clientLauncher.getRemoteProxy(); this.future = clientLauncher.startListening(); } private static PrintWriter getMessageTracer() { if ("true".equals(System.getProperty("sonarlint.debug.rpc"))) { try { return new PrintWriter(Paths.get(System.getProperty("user.home")).resolve(".sonarlint").resolve("rpc_client_session.log").toFile(), StandardCharsets.UTF_8); } catch (IOException e) { System.err.println("Cannot write rpc debug logs file"); e.printStackTrace(); } } return null; } public SonarLintRpcServer getServerProxy() { return serverProxy; } @Override public void close() { requestsExecutor.shutdown(); requestAndNotificationsSequentialExecutor.shutdown(); // Stop the MessageProducer thread future.cancel(true); messageReaderExecutor.shutdownNow(); messageWriterExecutor.shutdownNow(); try { if (!messageReaderExecutor.awaitTermination(10, TimeUnit.SECONDS)) { throw new IllegalStateException("Unable to terminate the client message reader thread in a timely manner"); } if (!messageWriterExecutor.awaitTermination(10, TimeUnit.SECONDS)) { throw new IllegalStateException("Unable to terminate the client message writer thread in a timely manner"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Interrupted!", e); } } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/ConfigScopeNotFoundException.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; public class ConfigScopeNotFoundException extends Exception { } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/ConnectionNotFoundException.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; public class ConnectionNotFoundException extends Exception { } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/Sloop.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.util.concurrent.CompletableFuture; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; public class Sloop { private final SonarLintRpcServer rpcServer; private final Process process; public Sloop(SonarLintRpcServer rpcServer, Process process) { this.rpcServer = rpcServer; this.process = process; } public CompletableFuture shutdown() { return rpcServer.shutdown(); } public SonarLintRpcServer getRpcServer() { return rpcServer; } /** * @return a future that will be completed when the process exits, providing the exit value */ public CompletableFuture onExit() { return process.onExit().thenApply(Process::exitValue); } public boolean isAlive() { return process.isAlive(); } public long getPid() { return process.pid(); } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SloopLauncher.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogLevel; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; public class SloopLauncher { public static final String SLOOP_CLI_ENTRYPOINT_CLASS = "org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli"; private final SonarLintRpcClientDelegate rpcClient; private final Function, ProcessBuilder> processBuilderFactory; private final Supplier osNameSupplier; public SloopLauncher(SonarLintRpcClientDelegate rpcClient) { this(rpcClient, ProcessBuilder::new, () -> System.getProperty("os.name")); } SloopLauncher(SonarLintRpcClientDelegate rpcClient, Function, ProcessBuilder> processBuilderFactory, Supplier osNameSupplier) { this.rpcClient = rpcClient; this.processBuilderFactory = processBuilderFactory; this.osNameSupplier = osNameSupplier; } public Sloop start(Path distPath) { return start(distPath, null); } public Sloop start(Path distPath, @Nullable Path jrePath) { return start(distPath, jrePath, null); } /** * @param jvmOpts Each argument should be separated by a space, such as '-XX:+UseG1GC -XX:MaxHeapFreeRatio=50' */ public Sloop start(Path distPath, @Nullable Path jrePath, @Nullable String jvmOpts) { try { return execute(distPath, jrePath, jvmOpts); } catch (Exception e) { logToClient(LogLevel.ERROR, "Unable to start the SonarLint backend", stackTraceToString(e)); throw new IllegalStateException("Unable to start the SonarLint backend", e); } } private static String stackTraceToString(Throwable t) { var stringWriter = new StringWriter(); var printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); return stringWriter.toString(); } /** * Inspired from Apache commons-lang3 */ private boolean isWindows() { var osName = osNameSupplier.get(); if (osName == null) { return false; } return osName.startsWith("Windows"); } private Sloop execute(Path distPath, @Nullable Path jrePath, @Nullable String jvmOpts) throws IOException { var jreHomePath = jrePath == null ? distPath.resolve("jre") : jrePath; logToClient(LogLevel.INFO, "Using JRE from " + jreHomePath, null); var binDirPath = jreHomePath.resolve("bin"); var jreJavaExePath = binDirPath.resolve("java" + (isWindows() ? ".exe" : "")); if (!Files.exists(jreJavaExePath)) { throw new IllegalArgumentException("The provided JRE path does not exist: " + jreJavaExePath); } var processBuilder = processBuilderFactory.apply(createCommand(distPath, jreJavaExePath, jvmOpts)); processBuilder.directory(binDirPath.toFile()); processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE); processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE); processBuilder.redirectError(ProcessBuilder.Redirect.PIPE); var process = processBuilder.start(); // redirect process.getErrorStream() to the client logs new StreamGobbler(process.getErrorStream(), stdErrLogConsumer()).start(); // use process.getInputStream() as an input for the client var serverToClientInputStream = process.getInputStream(); // use process.getOutputStream() as the standard input of a subprocess that can be written to var clientToServerOutputStream = process.getOutputStream(); var clientLauncher = new ClientJsonRpcLauncher(serverToClientInputStream, clientToServerOutputStream, rpcClient); process.onExit().thenAccept(p -> clientLauncher.close()); var serverProxy = clientLauncher.getServerProxy(); return new Sloop(serverProxy, process); } private static List createCommand(Path distPath, Path jreJavaExePath, @Nullable String clientJvmOpts) { var libFolderPath = distPath.resolve("lib"); var classpath = libFolderPath.toAbsolutePath().normalize() + File.separator + '*'; List commands = new ArrayList<>(); commands.add(jreJavaExePath.toAbsolutePath().normalize().toString()); var sonarlintEnvJvmOpts = System.getenv("SONARLINT_JVM_OPTS"); if (sonarlintEnvJvmOpts != null) { commands.addAll(Arrays.asList(sonarlintEnvJvmOpts.split(" "))); } if (clientJvmOpts != null) { commands.addAll(Arrays.asList(clientJvmOpts.split(" "))); } // Avoid displaying the Java icon in the taskbar on Mac commands.add("-Djava.awt.headless=true"); commands.add("-classpath"); commands.add(classpath); commands.add(SLOOP_CLI_ENTRYPOINT_CLASS); return commands; } private Consumer stdErrLogConsumer() { return s -> logToClient(LogLevel.ERROR, "StdErr: " + s, null); } private void logToClient(LogLevel level, @Nullable String message, @Nullable String stacktrace) { rpcClient.log(new LogParams(level, message, null, Thread.currentThread().getName(), SloopLauncher.class.getName(), stacktrace, Instant.now())); } private static class StreamGobbler extends Thread { private final InputStream inputStream; private final Consumer consumer; public StreamGobbler(InputStream inputStream, Consumer consumer) { this.inputStream = inputStream; this.consumer = consumer; } @Override public void run() { new BufferedReader(new InputStreamReader(inputStream)).lines() .forEach(consumer); } } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintCancelChecker.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.util.concurrent.CancellationException; /* * A class to use in place of {@link org.eclipse.lsp4j.jsonrpc.CancelChecker} to stop depending on lsp4j types in API * and services. * See SLCORE-663 for details. */ public class SonarLintCancelChecker { private final org.eclipse.lsp4j.jsonrpc.CancelChecker lsp4JCancelChecker; public SonarLintCancelChecker(org.eclipse.lsp4j.jsonrpc.CancelChecker cancelChecker) { this.lsp4JCancelChecker = cancelChecker; } /** * Throw a {@link CancellationException} if the currently processed request * has been canceled. */ public void checkCanceled() { lsp4JCancelChecker.checkCanceled(); } /** * Check for cancellation without throwing an exception. */ public boolean isCanceled() { return lsp4JCancelChecker.isCanceled(); } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientDelegate.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.net.URI; import java.net.URL; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CancellationException; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.plugin.PluginStatusDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.DependencyRiskDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.embeddedserver.EmbeddedServerStartedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.event.DidReceiveServerHotspotEvent; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.ProxyDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.X509CertificateDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageActionItem; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowSoonUnsupportedMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidSkipLoadingPluginParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ReportProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.StartProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.Language; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; /** * This is the interface that should be implemented by Java clients. We are trying to decouple from the RPC framework as much as possible, * but most of those methods should be pretty similar to {@link org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient}. * The "delegation" is made in {@link SonarLintRpcClientImpl} */ public interface SonarLintRpcClientDelegate { /** * Suggest a list of binding suggestions for each eligible configuration scope, * based on registered connections, config scope, binding clues, and git remote URL. * Scopes without any available suggestions are automatically excluded from the results. */ void suggestBinding(Map> suggestionsByConfigScope); void suggestConnection(Map> suggestionsByConfigScope); void openUrlInBrowser(URL url); /** * Display a message to the user, usually in a small notification. * The message is informative and does not imply applying an action. */ void showMessage(MessageType type, String text); /** * Display a message to the user, usually in a small notification. * This message has options that the user can pick from. Once user clicked on option, its String key is returned. * If the user explicitly dismisses/closes the notification without clicking option, then it returns response with null selectedKey and closedByUser set to true. * IMPORTANT: As users might not react to the notification at all, the returned future might block for an indefinite amount of time. * So the caller should not block waiting for the result, but provide a callback instead. */ default ShowMessageRequestResponse showMessageRequest(MessageType type, String text, List actions) { return null; } void log(LogParams params); /** * Display a one-time message to the user as a small notification. * The message is informative and a link to the documentation should be available. * The one-time mechanism should be handled on the client side (via a "Don't show again" button for example). * There is an in-memory cache for the pair of connection ID + version that were already seen on the core side, but it is cleared after each restart. */ void showSoonUnsupportedMessage(ShowSoonUnsupportedMessageParams params); void showSmartNotification(ShowSmartNotificationParams params); /** * Return the client dynamic description. * @see SonarLintRpcClient#getClientLiveInfo() */ String getClientLiveDescription(); void showHotspot(String configurationScopeId, HotspotDetailsDto hotspotDetails); /** * Sends a notification to the client to show a specific issue in the IDE */ void showIssue(String configurationScopeId, IssueDetailsDto issueDetails); /** * Sends a notification to the client to show a fix suggestion for a specific issue in the IDE * The fix is only on a single files, but it may contain different locations */ default void showFixSuggestion(String configurationScopeId, String issueKey, FixSuggestionDto fixSuggestion) { } /** * Can be triggered by the backend when trying to handle a feature that needs a connection, e.g. open hotspot. * @return the response to this connection creation assist request, that contains the new connection. The client can cancel the request if the user stops the creation process. * @throws java.util.concurrent.CancellationException if the client cancels the process */ AssistCreatingConnectionResponse assistCreatingConnection(AssistCreatingConnectionParams params, SonarLintCancelChecker cancelChecker) throws CancellationException; /** * Can be triggered by the backend when trying to handle a feature that needs a bound project, e.g. open hotspot. * @return the response to this binding assist request, that contains the bound project. The client can cancel the request if the user stops the binding process. * @throws java.util.concurrent.CancellationException if the client cancels the process */ AssistBindingResponse assistBinding(AssistBindingParams params, SonarLintCancelChecker cancelChecker) throws CancellationException; /** * Requests the client to start showing progress to users. * @throws UnsupportedOperationException if there is an error while creating the corresponding UI */ void startProgress(StartProgressParams params) throws UnsupportedOperationException; /** * Reports progress to the client. */ void reportProgress(ReportProgressParams params); void didSynchronizeConfigurationScopes(Set configurationScopeIds); /** * @throws ConnectionNotFoundException if the connection doesn't exist on the client side * @return null if no credentials are available for this connection (backend may use unauthenticated HTTP requests) */ @CheckForNull Either getCredentials(String connectionId) throws ConnectionNotFoundException; List selectProxies(URI uri); GetProxyPasswordAuthenticationResponse getProxyPasswordAuthentication(String host, int port, String protocol, String prompt, String scheme, URL targetHost); /** * @param chain the peer certificate chain * @param authType the key exchange algorithm used */ boolean checkServerTrusted(List chain, String authType); @Deprecated(since = "10.3") default void didReceiveServerHotspotEvent(DidReceiveServerHotspotEvent params) { // no-op } /** * @return null if the client is unable to match the branch */ @CheckForNull String matchSonarProjectBranch(String configurationScopeId, String mainBranchName, Set allBranchesNames, SonarLintCancelChecker cancelChecker) throws ConfigScopeNotFoundException; @Deprecated(since = "10.23", forRemoval = true) default boolean matchProjectBranch(String configurationScopeId, String branchNameToMatch, SonarLintCancelChecker cancelChecker) { return true; } void didChangeMatchedSonarProjectBranch(String configScopeId, String newMatchedBranchName); TelemetryClientLiveAttributesResponse getTelemetryLiveAttributes(); void didChangeTaintVulnerabilities(String configurationScopeId, Set closedTaintVulnerabilityIds, List addedTaintVulnerabilities, List updatedTaintVulnerabilities); default void didChangeDependencyRisks(String configurationScopeId, Set closedDependencyRiskIds, List addedDependencyRisks, List updatedDependencyRisks) { } default Path getBaseDir(String configurationScopeId) throws ConfigScopeNotFoundException { return null; } List listFiles(String configScopeId) throws ConfigScopeNotFoundException; void noBindingSuggestionFound(NoBindingSuggestionFoundParams params); void didChangeAnalysisReadiness(Set configurationScopeIds, boolean areReadyForAnalysis); default void raiseIssues(String configurationScopeId, Map> issuesByFileUri, boolean isIntermediatePublication, @Nullable UUID analysisId) { } default void raiseHotspots(String configurationScopeId, Map> hotspotsByFileUri, boolean isIntermediatePublication, @Nullable UUID analysisId) { } default void didSkipLoadingPlugin(String configurationScopeId, Language language, DidSkipLoadingPluginParams.SkipReason reason, String minVersion, @Nullable String currentVersion) { } default void didDetectSecret(String configurationScopeId) { } default void promoteExtraEnabledLanguagesInConnectedMode(String configurationScopeId, Set languagesToPromote) { } default Map getInferredAnalysisProperties(String configurationScopeId, List filesToAnalyze) throws ConfigScopeNotFoundException { return Map.of(); } default Set getFileExclusions(String configurationScopeId) throws ConfigScopeNotFoundException { return Collections.emptySet(); } default void invalidToken(String connectionId) { } default void embeddedServerStarted(EmbeddedServerStartedParams params) { } /** * Called whenever the status of one or more analyzer plugins changes for a specific config scope. * This can happen when plugins are loaded, unloaded, synced from a connection, or when a connection is removed. * The parameters contain the full updated list of plugin statuses (one entry per known language) for the given scope. * Clients should use this to refresh the "Supported Languages" panel for the corresponding project. */ default void didChangePluginStatuses(String configScopeId, List pluginStatuses) { } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImpl.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.io.PrintWriter; import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.time.Instant; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.eclipse.lsp4j.jsonrpc.CompletableFutures; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode; import org.sonarsource.sonarlint.core.rpc.protocol.client.OpenUrlInBrowserParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.DidChangeAnalysisReadinessParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.DidDetectSecretParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetFileExclusionsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetFileExclusionsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetInferredAnalysisPropertiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.analysis.GetInferredAnalysisPropertiesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.SuggestBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.DidChangeMatchedSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchSonarProjectBranchResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetCredentialsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.GetCredentialsResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.SuggestConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.embeddedserver.EmbeddedServerStartedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.event.DidReceiveServerHotspotEvent; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.ShowFixSuggestionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.GetBaseDirParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.GetBaseDirResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.ListFilesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.fs.ListFilesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaiseHotspotsParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.ShowHotspotParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.CheckServerTrustedParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.CheckServerTrustedResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.SelectProxiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.SelectProxiesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.info.GetClientLiveInfoResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaiseIssuesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.ShowIssueParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogLevel; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowMessageRequestResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowSoonUnsupportedMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidChangePluginStatusesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.plugin.DidSkipLoadingPluginParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ReportProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.StartProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.promotion.PromoteExtraEnabledLanguagesInConnectedModeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sca.DidChangeDependencyRisksParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.DidSynchronizeConfigurationScopeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.sync.InvalidTokenParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.taint.vulnerability.DidChangeTaintVulnerabilitiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; /** * Implementation of {@link SonarLintRpcClient} that delegates to {@link SonarLintRpcClientDelegate} in order to simplify Java clients and avoid * leaking too many RPC-specific concept in each Java IDE. * In particular, this class attempt to: *
    *
  • Hide the fact that RPC is asynchronous (don't let clients manipulate completable futures)
  • *
  • Hide cancellation except if there is a functional need
  • *
  • Convert Java exceptions to RPC error messages
  • *
*/ public class SonarLintRpcClientImpl implements SonarLintRpcClient { private final SonarLintRpcClientDelegate delegate; private final Executor requestsExecutor; private final Executor requestAndNotificationsSequentialExecutor; public SonarLintRpcClientImpl(SonarLintRpcClientDelegate delegate, Executor requestsExecutor, Executor requestAndNotificationsSequentialExecutor) { this.delegate = delegate; this.requestsExecutor = requestsExecutor; this.requestAndNotificationsSequentialExecutor = requestAndNotificationsSequentialExecutor; } protected CompletableFuture requestAsync(Function code) { CompletableFuture start = new CompletableFuture<>(); // First we schedule the processing of the request on the sequential executor, to maintain ordering of notifications, requests, responses, // and cancellations var sequentialFuture = start.thenApplyAsync(cancelChecker -> { // We can maybe cancel early cancelChecker.checkCanceled(); return cancelChecker; }, requestAndNotificationsSequentialExecutor); // Then requests are processed asynchronously to not block the processing of notifications, responses and cancellations var requestFuture = sequentialFuture.thenApplyAsync(cancelChecker -> { cancelChecker.checkCanceled(); return code.apply(cancelChecker); }, requestsExecutor); start.complete(new CompletableFutures.FutureCancelChecker(requestFuture)); return requestFuture; } protected CompletableFuture runAsync(Consumer code) { CompletableFuture start = new CompletableFuture<>(); // First we schedule the processing of the request on the sequential executor, to maintain ordering of notifications, requests, responses, // and cancellations var sequentialFuture = start.thenApplyAsync(cancelChecker -> { // We can maybe cancel early cancelChecker.checkCanceled(); return cancelChecker; }, requestAndNotificationsSequentialExecutor); // Then requests are processed asynchronously to not block the processing of notifications, responses and cancellations var requestFuture = sequentialFuture.thenApplyAsync(cancelChecker -> { cancelChecker.checkCanceled(); code.accept(cancelChecker); return null; }, requestsExecutor); start.complete(new CompletableFutures.FutureCancelChecker(requestFuture)); return requestFuture; } protected void notify(Runnable code) { requestAndNotificationsSequentialExecutor.execute(() -> { try { code.run(); } catch (Throwable throwable) { logClientSideError("Error when handling a notification", throwable); } }); } /** * Client errors don't need to go over RPC, and can instead directly go through the delegate. */ void logClientSideError(String message, Throwable throwable) { delegate.log(new LogParams(LogLevel.ERROR, message, null, stackTraceToString(throwable), Instant.now())); } private static String stackTraceToString(Throwable t) { var stringWriter = new StringWriter(); var printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); return stringWriter.toString(); } @Override public void suggestBinding(SuggestBindingParams params) { notify(() -> delegate.suggestBinding(params.getSuggestions())); } @Override public void suggestConnection(SuggestConnectionParams params) { notify(() -> delegate.suggestConnection(params.getSuggestionsByConfigScopeId())); } @Override public void openUrlInBrowser(OpenUrlInBrowserParams params) { notify(() -> { try { delegate.openUrlInBrowser(URI.create(params.getUrl()).toURL()); } catch (MalformedURLException | IllegalArgumentException e) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "Not a valid URL: " + params.getUrl(), params.getUrl())); } }); } @Override public void showMessage(ShowMessageParams params) { notify(() -> delegate.showMessage(params.getType(), params.getText())); } @Override public CompletableFuture showMessageRequest(ShowMessageRequestParams params) { return requestAsync(cancelChecker -> delegate.showMessageRequest(params.getType(), params.getMessage(), params.getActions())); } @Override public void log(LogParams params) { notify(() -> delegate.log(params)); } @Override public void showSoonUnsupportedMessage(ShowSoonUnsupportedMessageParams params) { notify(() -> delegate.showSoonUnsupportedMessage(params)); } @Override public void showSmartNotification(ShowSmartNotificationParams params) { notify(() -> delegate.showSmartNotification(params)); } @Override public CompletableFuture getClientLiveInfo() { return requestAsync(cancelChecker -> new GetClientLiveInfoResponse(delegate.getClientLiveDescription())); } @Override public void showHotspot(ShowHotspotParams params) { notify(() -> delegate.showHotspot(params.getConfigurationScopeId(), params.getHotspotDetails())); } @Override public void showIssue(ShowIssueParams params) { notify(() -> delegate.showIssue(params.getConfigurationScopeId(), params.getIssueDetails())); } @Override public void showFixSuggestion(ShowFixSuggestionParams params) { notify(() -> delegate.showFixSuggestion(params.getConfigurationScopeId(), params.getIssueKey(), params.getFixSuggestion())); } @Override public CompletableFuture assistCreatingConnection(AssistCreatingConnectionParams params) { return requestAsync(cancelChecker -> delegate.assistCreatingConnection(params, new SonarLintCancelChecker(cancelChecker))); } @Override public CompletableFuture assistBinding(AssistBindingParams params) { return requestAsync(cancelChecker -> delegate.assistBinding(params, new SonarLintCancelChecker(cancelChecker))); } @Override public CompletableFuture startProgress(StartProgressParams params) { return runAsync(cancelChecker -> { try { delegate.startProgress(params); } catch (UnsupportedOperationException e) { throw new ResponseErrorException(new ResponseError(SonarLintRpcErrorCode.PROGRESS_CREATION_FAILED, e.getMessage(), null)); } }); } @Override public void reportProgress(ReportProgressParams params) { notify(() -> delegate.reportProgress(params)); } @Override public void didSynchronizeConfigurationScopes(DidSynchronizeConfigurationScopeParams params) { notify(() -> delegate.didSynchronizeConfigurationScopes(params.getConfigurationScopeIds())); } @Override public CompletableFuture getCredentials(GetCredentialsParams params) { return requestAsync(cancelChecker -> { try { return new GetCredentialsResponse(delegate.getCredentials(params.getConnectionId())); } catch (ConnectionNotFoundException e) { throw new ResponseErrorException( new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Unknown connection: " + params.getConnectionId(), params.getConnectionId())); } }); } @Override public CompletableFuture getTelemetryLiveAttributes() { return requestAsync(cancelChecker -> delegate.getTelemetryLiveAttributes()); } @Override public CompletableFuture selectProxies(SelectProxiesParams params) { return requestAsync(cancelChecker -> new SelectProxiesResponse(delegate.selectProxies(params.getUri()))); } @Override public CompletableFuture getProxyPasswordAuthentication(GetProxyPasswordAuthenticationParams params) { return requestAsync(cancelChecker -> delegate.getProxyPasswordAuthentication(params.getHost(), params.getPort(), params.getProtocol(), params.getPrompt(), params.getScheme(), params.getTargetHost())); } @Override public CompletableFuture checkServerTrusted(CheckServerTrustedParams params) { return requestAsync(cancelChecker -> new CheckServerTrustedResponse(delegate.checkServerTrusted(params.getChain(), params.getAuthType()))); } @Override public void didReceiveServerHotspotEvent(DidReceiveServerHotspotEvent params) { notify(() -> delegate.didReceiveServerHotspotEvent(params)); } @Override public CompletableFuture matchSonarProjectBranch(MatchSonarProjectBranchParams params) { return requestAsync(cancelChecker -> { try { return new MatchSonarProjectBranchResponse( delegate.matchSonarProjectBranch(params.getConfigurationScopeId(), params.getMainSonarBranchName(), params.getAllSonarBranchesNames(), new SonarLintCancelChecker(cancelChecker))); } catch (ConfigScopeNotFoundException e) { throw configScopeNotFoundError(params.getConfigurationScopeId()); } }); } @Override public void didChangeMatchedSonarProjectBranch(DidChangeMatchedSonarProjectBranchParams params) { notify(() -> delegate.didChangeMatchedSonarProjectBranch(params.getConfigScopeId(), params.getNewMatchedBranchName())); } @Override public CompletableFuture getBaseDir(GetBaseDirParams params) { return requestAsync(cancelChecker -> { try { return new GetBaseDirResponse(delegate.getBaseDir(params.getConfigurationScopeId())); } catch (ConfigScopeNotFoundException e) { throw configScopeNotFoundError(params.getConfigurationScopeId()); } }); } @Override public CompletableFuture listFiles(ListFilesParams params) { return requestAsync(cancelChecker -> { try { return new ListFilesResponse(delegate.listFiles(params.getConfigScopeId())); } catch (ConfigScopeNotFoundException e) { throw configScopeNotFoundError(params.getConfigScopeId()); } }); } private static ResponseErrorException configScopeNotFoundError(String configScopeId) { return new ResponseErrorException( new ResponseError(SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_FOUND, "Unknown config scope: " + configScopeId, configScopeId)); } @Override public void didChangeTaintVulnerabilities(DidChangeTaintVulnerabilitiesParams params) { notify(() -> delegate.didChangeTaintVulnerabilities(params.getConfigurationScopeId(), params.getClosedTaintVulnerabilityIds(), params.getAddedTaintVulnerabilities(), params.getUpdatedTaintVulnerabilities())); } @Override public void didChangeDependencyRisks(DidChangeDependencyRisksParams params) { notify(() -> delegate.didChangeDependencyRisks(params.getConfigurationScopeId(), params.getClosedDependencyRiskIds(), params.getAddedDependencyRisks(), params.getUpdatedDependencyRisks())); } @Override public void noBindingSuggestionFound(NoBindingSuggestionFoundParams params) { notify(() -> delegate.noBindingSuggestionFound(params)); } public void didChangeAnalysisReadiness(DidChangeAnalysisReadinessParams params) { notify(() -> delegate.didChangeAnalysisReadiness(params.getConfigurationScopeIds(), params.areReadyForAnalysis())); } @Override public void raiseIssues(RaiseIssuesParams params) { notify(() -> delegate.raiseIssues(params.getConfigurationScopeId(), params.getIssuesByFileUri(), params.isIntermediatePublication(), params.getAnalysisId())); } @Override public void raiseHotspots(RaiseHotspotsParams params) { notify(() -> delegate.raiseHotspots(params.getConfigurationScopeId(), params.getHotspotsByFileUri(), params.isIntermediatePublication(), params.getAnalysisId())); } @Override public void didSkipLoadingPlugin(DidSkipLoadingPluginParams params) { notify(() -> delegate.didSkipLoadingPlugin(params.getConfigurationScopeId(), params.getLanguage(), params.getReason(), params.getMinVersion(), params.getCurrentVersion())); } @Override public void didDetectSecret(DidDetectSecretParams params) { notify(() -> delegate.didDetectSecret(params.getConfigurationScopeId())); } @Override public void promoteExtraEnabledLanguagesInConnectedMode(PromoteExtraEnabledLanguagesInConnectedModeParams params) { notify(() -> delegate.promoteExtraEnabledLanguagesInConnectedMode(params.getConfigurationScopeId(), params.getLanguagesToPromote())); } @Override public CompletableFuture getInferredAnalysisProperties(GetInferredAnalysisPropertiesParams params) { return requestAsync(cancelChecker -> { try { return new GetInferredAnalysisPropertiesResponse(delegate.getInferredAnalysisProperties(params.getConfigurationScopeId(), params.getFilesToAnalyze())); } catch (ConfigScopeNotFoundException e) { throw configScopeNotFoundError(params.getConfigurationScopeId()); } }); } @Override public CompletableFuture getFileExclusions(GetFileExclusionsParams params) { return requestAsync(cancelChecker -> { try { return new GetFileExclusionsResponse(delegate.getFileExclusions(params.getConfigurationScopeId())); } catch (ConfigScopeNotFoundException e) { throw configScopeNotFoundError(params.getConfigurationScopeId()); } }); } @Override public void invalidToken(InvalidTokenParams params) { notify(() -> delegate.invalidToken(params.getConnectionId())); } @Override public void embeddedServerStarted(EmbeddedServerStartedParams params) { notify(() -> delegate.embeddedServerStarted(params)); } @Override public void didChangePluginStatuses(DidChangePluginStatusesParams params) { notify(() -> delegate.didChangePluginStatuses(params.getConfigScopeId(), params.getPluginStatuses())); } } ================================================ FILE: client/rpc-java-client/src/main/java/org/sonarsource/sonarlint/core/rpc/client/package-info.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ @ParametersAreNonnullByDefault package org.sonarsource.sonarlint.core.rpc.client; import javax.annotation.ParametersAreNonnullByDefault; ================================================ FILE: client/rpc-java-client/src/test/java/org/sonarsource/sonarlint/core/rpc/client/SloopLauncherTests.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogLevel; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SloopLauncherTests { private Process mockProcess; private SloopLauncher underTest; private Sloop sloop; private Function, ProcessBuilder> mockPbFactory; private SonarLintRpcClientDelegate rpcClient; private String osName = "Linux"; private Path fakeJreHomePath; private Path fakeJreJavaLinuxPath; private Path fakeJreJavaWindowsPath; @BeforeEach void prepare(@TempDir Path fakeJreHomePath) throws IOException { this.fakeJreHomePath = fakeJreHomePath; var fakeJreBinFolder = this.fakeJreHomePath.resolve("bin"); Files.createDirectories(fakeJreBinFolder); fakeJreJavaLinuxPath = fakeJreBinFolder.resolve("java"); Files.createFile(fakeJreJavaLinuxPath); fakeJreJavaWindowsPath = fakeJreBinFolder.resolve("java.exe"); Files.createFile(fakeJreJavaWindowsPath); mockPbFactory = mock(); var mockProcessBuilder = mock(ProcessBuilder.class); when(mockPbFactory.apply(any())).thenReturn(mockProcessBuilder); mockProcess = mock(Process.class); when(mockProcess.onExit()).thenReturn(new CompletableFuture<>()); doReturn(mockProcess).when(mockProcessBuilder).start(); when(mockProcess.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[0])); when(mockProcess.getErrorStream()).thenReturn(new ByteArrayInputStream(new byte[0])); when(mockProcess.getOutputStream()).thenReturn(new ByteArrayOutputStream()); rpcClient = mock(SonarLintRpcClientDelegate.class); underTest = new SloopLauncher(rpcClient, mockPbFactory, () -> osName); } @Test void test_command_with_embedded_jre(@TempDir Path distPath) throws IOException { var bundledJreBinPath = distPath.resolve("jre").resolve("bin"); Files.createDirectories(bundledJreBinPath); var bundledJrejavaPath = bundledJreBinPath.resolve("java"); Files.createFile(bundledJrejavaPath); sloop = underTest.start(distPath); verify(mockPbFactory).apply(List.of(bundledJrejavaPath.toString(), "-Djava.awt.headless=true", "-classpath", distPath.resolve("lib") + File.separator + '*', "org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli")); assertThat(sloop.getRpcServer()).isNotNull(); } @Test void test_command_with_custom_jre_on_linux(@TempDir Path distPath) { sloop = underTest.start(distPath, fakeJreHomePath); verify(mockPbFactory) .apply(List.of(fakeJreJavaLinuxPath.toString(), "-Djava.awt.headless=true", "-classpath", distPath.resolve("lib") + File.separator + '*', "org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli")); assertThat(sloop.getRpcServer()).isNotNull(); } @Test void test_command_with_custom_jre_on_windows(@TempDir Path distPath) { osName = "Windows"; sloop = underTest.start(distPath, fakeJreHomePath); verify(mockPbFactory) .apply(List.of(fakeJreJavaWindowsPath.toString(), "-Djava.awt.headless=true", "-classpath", distPath.resolve("lib") + File.separator + '*', "org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli")); assertThat(sloop.getRpcServer()).isNotNull(); } @Test void test_redirect_stderr_to_client(@TempDir Path distPath) { when(mockProcess.getErrorStream()).thenReturn(new ByteArrayInputStream("Some errors\nSome other error".getBytes())); sloop = underTest.start(distPath, fakeJreHomePath); ArgumentCaptor captor = ArgumentCaptor.captor(); verify(rpcClient, timeout(1000).times(3)).log(captor.capture()); assertThat(captor.getAllValues()) .filteredOn(m -> m.getLevel() == LogLevel.ERROR) .extracting(LogParams::getMessage) .containsExactly("StdErr: Some errors", "StdErr: Some other error"); } @Test void test_log_stacktrace(@TempDir Path distPath) { doThrow(new IllegalStateException("Some error")).when(mockProcess).getInputStream(); assertThrows(IllegalStateException.class, () -> sloop = underTest.start(distPath, fakeJreHomePath)); ArgumentCaptor captor = ArgumentCaptor.captor(); verify(rpcClient, times(2)).log(captor.capture()); var log = captor.getValue(); assertThat(log.getMessage()).isEqualTo("Unable to start the SonarLint backend"); assertThat(log.getStackTrace()).startsWith("java.lang.IllegalStateException: Some error"); } @Test void test_throw_error_if_java_path_does_not_exist(@TempDir Path distPath) { var wrongPath = Paths.get("wrongPath"); assertThrows(IllegalStateException.class, () -> sloop = underTest.start(distPath, wrongPath)); } @Test void test_command_with_custom_jre_on_linux_and_jvm_option(@TempDir Path distPath) { sloop = underTest.start(distPath, fakeJreHomePath, "-XX:+UseG1GC -XX:MaxHeapFreeRatio=50"); verify(mockPbFactory) .apply(List.of(fakeJreJavaLinuxPath.toString(), "-XX:+UseG1GC", "-XX:MaxHeapFreeRatio=50", "-Djava.awt.headless=true", "-classpath", distPath.resolve("lib") + File.separator + '*', "org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli")); assertThat(sloop.getRpcServer()).isNotNull(); } } ================================================ FILE: client/rpc-java-client/src/test/java/org/sonarsource/sonarlint/core/rpc/client/SonarLintRpcClientImplTest.java ================================================ /* * SonarLint Core - RPC Java Client * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.sonarlint.core.rpc.client; import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.sonarsource.sonarlint.core.rpc.protocol.client.branch.MatchProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; class SonarLintRpcClientImplTest { @Test void it_should_print_notification_handling_errors_to_the_client_logs() { var fakeClientDelegate = mock(SonarLintRpcClientDelegate.class); var argumentCaptor = ArgumentCaptor.forClass(LogParams.class); var rpcClient = new SonarLintRpcClientImpl(fakeClientDelegate, Runnable::run, Runnable::run); rpcClient.notify(() -> { throw new IllegalStateException("Kaboom"); }); verify(fakeClientDelegate).log(argumentCaptor.capture()); assertThat(argumentCaptor.getAllValues()) .anySatisfy(logParam -> { assertThat(logParam.getMessage()).contains("Error when handling a notification"); assertThat(logParam.getStackTrace()).contains("java.lang.IllegalStateException: Kaboom"); }); } @Test void it_should_match_project_branch() throws ExecutionException, InterruptedException { var fakeClientDelegate = mock(SonarLintRpcClientDelegate.class); var rpcClient = new SonarLintRpcClientImpl(fakeClientDelegate, Runnable::run, Runnable::run); var params = new MatchProjectBranchParams("configScopeId", "branch"); var response = rpcClient.matchProjectBranch(params); assertThat(params.getConfigurationScopeId()).isEqualTo("configScopeId"); assertThat(params.getServerBranchToMatch()).isEqualTo("branch"); assertThat(response.get().isBranchMatched()).isTrue(); } } ================================================ FILE: doc/analyzer_management.md ================================================ # Artifact Management in SonarLint Core This document explains how SQ:IDE manages the artifacts (plugins and plugin dependencies) required to analyze code. It covers where artifacts come from, how they are loaded, and how the loading strategy adapts to standalone vs. connected mode. --- ## Glossary | Term | Meaning | |------|---------| | **Plugin** | A standard SonarSource analyzer packaged as a JAR. Loaded by the analysis engine via the plugin API. | | **Plugin dependency / sidecar** | An artifact required for a plugin to work but not itself a plugin (e.g. an OmniSharp distribution for the C# analyzer). Deployed on binaries.sonarsource.com alongside plugins. | | **Artifact** | Umbrella term for both plugins and plugin dependencies. | | **Artifact origin** | Where an artifact physically came from: `EMBEDDED`, `ON_DEMAND`, `SONARQUBE_SERVER`, `SONARQUBE_CLOUD`. Represented by the `ArtifactOrigin` enum. | --- ## Motivation Historically SQ:IDE shipped every language analyzer bundled inside the IDE extension. This made the extension large and required a release each time a new analyzer version was needed. Two changes were made to address this: 1. **On-demand source.** A curated set of artifacts (e.g. CFamily, C# OSS, OmniSharp) can be downloaded at runtime from `binaries.sonarsource.com`, reducing the size of the shipped extension. This also enables plugin _dependencies_ to be distributed alongside plugins on the same infrastructure. 2. **Policy-based loading.** Rather than having a single monolithic resolver, artifact loading is now split between *where artifacts come from* (`ArtifactSource`) and *how sources are combined* (`ArtifactsLoadingStrategy`). --- ## Core Abstractions ### `ArtifactSource` Represents **one place where artifacts can be obtained**. Every source exposes two methods: ``` listAvailableArtifacts(Set enabledLanguages) → List Returns all artifacts known to this source for the given set of enabled languages, without triggering any downloads. This is a pure query. Implementations should return artifacts corresponding to enabled languages, and artifacts that are not tied to a specific language. load(String artifactKey) → Optional Action. Ensures the artifact is available, scheduling a background download if needed. Returns empty if this source does not handle the given key. May return a ResolvedArtifact in DOWNLOADING state. ``` `ResolvedArtifact` captures the outcome: `(ArtifactState state, Path path, ArtifactOrigin origin, Version version)`. The state is one of: `ACTIVE`, `SYNCED`, `DOWNLOADING`, `FAILED`, `PREMIUM`, `UNSUPPORTED`. There are three concrete implementations: #### `EmbeddedPluginSource` Backed by JARs physically bundled in the IDE extension. Never triggers downloads. Covers both language plugins (e.g. `sonar-java-plugin.jar`) and companion plugins embedded by the client (e.g. `sonarlint-omnisharp-plugin.jar`). Two factory methods select the right set of paths: - `EmbeddedPluginSource.forStandalone(params)` — standalone embedded paths + optional C# OSS standalone JAR - `EmbeddedPluginSource.forConnected(params)` — connected-mode embedded paths only #### `BinariesArtifactSource` Backed by `binaries.sonarsource.com`. Handles both **plugins** (CFamily, C# OSS) and **plugin dependencies** (OmniSharp distributions). Artifacts are cached under `/ondemand-plugins/`. - `listAvailableArtifacts(Set enabledLanguages)` lists artifacts available on Binaries. - `load(key)` checks the cache first; if absent, schedules an async download and returns `DOWNLOADING` immediately. A `PluginStatusUpdateEvent` is published when the download finishes (with `ACTIVE` or `FAILED`). - Signature verification (`OnDemandPluginSignatureVerifier`) runs after every download. - Concurrent duplicate downloads for the same artifact are deduplicated (`UniqueTaskExecutor`). #### `ServerPluginSource` Backed by a specific SonarQube Server or SonarQube Cloud connection. One instance per connection, cached by `ConnectedArtifactsLoadingStrategyFactory`. - `listAvailableArtifacts(Set enabledLanguages)` returns what the server currently exposes (converted from the server plugin list). Falls back to an empty list if the server is unreachable. - `listServerPlugins()` returns the raw `ServerPlugin` list (richer metadata, used by the loading policy for skip-list and companion decisions). - `load(key)` returns `SYNCED` if the artifact is on disk with a matching hash, or schedules an async download (returns `DOWNLOADING`). Enterprise-variant resolution happens here. - `isAnyDownloadInProgress()` delegates to `ServerPluginDownloader`. --- ### `ArtifactsLoadingStrategy` Represents **how sources are combined** to produce the full set of resolved artifacts for a given context (standalone or connected). The interface exposes: ``` resolveArtifacts() → ArtifactsLoadingResult Resolves all artifacts from all managed sources, applying priority and policy rules. The result holds the resolved artifact map (keyed by artifact key) and the set of enabled languages. May schedule background downloads. ``` There are two implementations: #### `StandaloneArtifactsLoadingStrategy` Used when the user has no server connection. Sources (in ascending priority): | Priority | Source | Why | |----------|-------------------------------------------|----------------------------------------------| | Lowest | `BinariesArtifactSource` | Fallback for on-demand artifacts | | Highest | `EmbeddedPluginSource` (standalone paths) | Overrides on-demand when embedded is present | Languages available only in connected mode are reported as `PREMIUM` (no artifact path, just a status indicating the language requires a server connection). Resolution uses a winner-map (ascending priority, last writer wins per key), then one post-processing pass: - For each `SonarLanguage` whose plugin key is still unresolved and is connected-only: mark as `PREMIUM`. #### `ConnectedArtifactsLoadingStrategy` Used when the user has a server connection. One instance per connection, cached by `ConnectedArtifactsLoadingStrategyFactory`. Sources (in ascending priority): | Priority | Source | Why | |----------|--------|-----| | Lowest | `BinariesArtifactSource` | Fallback for on-demand artifacts | | Middle | `ServerPluginSource` | Server-specific analyzers override binaries | | Highest | `EmbeddedPluginSource` (connected paths) | JARs the client always carries (normally win) | Resolution uses a winner-map (ascending priority, last writer wins per key), then two post-processing passes: 1. **Enterprise-variant deduplication**: when a different-key enterprise variant (e.g. `csharpenterprise`) is present, the base key is removed so both are not loaded simultaneously. 2. **Enterprise priority override**: when the server reports a plugin as enterprise (same-key enterprise editions such as Go or IaC), the server source is forced for that key even if the embedded source would normally win. --- ## Artifact State Machine ``` load() called │ ▼ ┌───────────────┐ │ On disk? │──── Yes ──► ACTIVE / SYNCED └───────────────┘ │ No ▼ ┌───────────────┐ │ Schedule │ │ download │──────────► DOWNLOADING └───────────────┘ │ (async) ┌──────┴──────┐ ▼ ▼ ACTIVE FAILED (event fired) (event fired) ``` `PluginStatusUpdateEvent` is published on every transition out of `DOWNLOADING`. The IDE listens to these events to update the status bar. --- ## Step-by-Step Flow ### 1. Initialization When `SonarLintBackendImpl.initialize()` is called by the IDE extension, the Spring context boots. `PluginsService`, `BinariesArtifactSource`, and the connected-mode factory are instantiated as singletons. No artifacts are loaded yet. ### 2. Connection Sync (connected mode only) If the workspace has bound projects, `PluginsSynchronizer` hits `api/plugins/installed` to learn what the server exposes. This updates the local storage with expected plugin paths and hashes. ### 3. Artifact Resolution When analysis is requested (or when the IDE requests plugin statuses), `PluginsService` calls `resolveArtifacts()` on the appropriate `ArtifactsLoadingStrategy`. The strategy runs its resolution passes and returns an `ArtifactsLoadingResult`. Artifacts in `DOWNLOADING` state have a non-null `downloadFuture`. The caller uses `ArtifactsLoadingResult.whenAllArtifactsDownloaded()` to run a callback (publishing a `PluginsSynchronizedEvent`) once all pending downloads complete. ### 4. Background Download When `load()` on a source cannot find the artifact locally, it enqueues a background download: - `BinariesArtifactSource` uses `UniqueTaskExecutor` + HTTP fetch from `binaries.sonarsource.com`, followed by signature verification. - `ServerPluginSource` delegates to `ServerPluginDownloader`, which deduplicates concurrent downloads for the same connection. ### 5. Class Loading Once all required artifacts are in `ACTIVE` or `SYNCED` state, `PluginsService` passes the resolved JAR paths to the core `PluginsLoader`, creating isolated `ClassLoader` instances per plugin. Analysis can then run. --- ================================================ FILE: docs/PULL_REQUEST_TEMPLATE.md ================================================ # For SonarSourcers: - [ ] Prefix the commit message with the ticket number, i.e. `SLCORE-XXXX` if you already have a ticket in Jira - [ ] For standalone PRs without issue in Jira: - [ ] Mention Epic ID in this descrition to create a new Task in Jira - [ ] Mention Issue ID in this descrition to create a new Sub-Task in Jira - [ ] Do not mention any Jira issue to create a new Task in Jira without a parent - [ ] When changing an API: - [ ] Explain in the JavaDoc the purpose of the new API - [ ] Document the change in [API_CHANGES.md](https://github.com/SonarSource/sonarlint-core/blob/master/API_CHANGES.md) - [ ] If the change breaks the current API, explicitly communicate those to the impacted consumers prior to merging (eg. IDE squad) - [ ] Make sure the tests adhere to the convention: - [ ] All test method names should use `snake_case`, for example: `test_validate_input`. - [ ] Make sure checks are green: build passes, Quality Gate is green # For external contributors: In addition to the above, please review our [contribution guidelines](https://github.com/SonarSource/sonarlint-core/blob/master/docs/contributing.md) and ensure your pull request adheres to the following guidelines: - [ ] Please explain your motives to contribute this change: what problem you are trying to fix, what improvement you are trying to make - [ ] Use the following formatting style: [SonarSource/sonar-developer-toolset](https://github.com/SonarSource/sonar-developer-toolset#code-style) - [ ] Provide a unit test for any code you changed ================================================ FILE: docs/contributing.md ================================================ Contributing ============ If you would like to see a new feature, please create a new thread in the forum ["Suggest new features"](https://community.sonarsource.com/c/suggestions/features). Please be aware that we are not actively looking for feature contributions. The truth is that it's extremely difficult for someone outside SonarSource to comply with our roadmap and expectations. Therefore, we typically only accept minor cosmetic changes and typo fixes. With that in mind, if you would like to submit a code contribution, please create a pull request for this repository. Please explain your motives to contribute this change: what problem you are trying to fix, what improvement you are trying to make. Make sure that you follow our [code style](https://github.com/SonarSource/sonar-developer-toolset#code-style) and all tests are passing (Travis build is executed for each pull request). Thank You! The SonarSource Team ================================================ FILE: docs/decisions/0000-move-more-responsibilities-to-the-core.md ================================================ # Move more responsibilities from clients to the core # Why? SonarLint Core is a Java library, shared by the 3 "Java based" flavors of SonarLint (SLE, SLI, SLVSCODE). Its initial mission was to replace the scanner-engine: load and execute plugins. Then connected mode related code was added (WS + storage). A clear API for IDEs was initially maintained, but then we decided it was useless, since all IDEs were evolving under the control of the same people. The 2 main entry points are StandaloneSonarLintEngine and ConnectedSonarLintEngine. IDEs are responsible to instantiate as many of those "engines" as they need (1 standalone, 1 connected per connection). There are also a lot of small utility classes that are used by IDEs without using the "engines". The problems: 1. API STABILITY: different squads are consumer of SonarLint Core, having a very unstable API is not comfortable 2. DUPLICATION: latest added features require an overall view of the "engines", so we had no choice but to put the logic in each IDEs. Exemple, when an open hotspot request arrives, we need to find the connection it refers to. This logic can't happen in the core today. => lot of duplicated logic: * open hotspots * telemetry * engine management (start/stop/sync/…) * binding management (bind/unbind/compute prefixes/…) * VCS branch management * analysis scheduling * local issue tracking 3. RUNTIME / TECH STACK: relying on IDE runtime to execute most of our logic (including analysis) is a blocker for innovation. people are still asking support for Java 8 runtime in Cobol IDEs hard to use Kotlin libraries in OSGi context hiring would be more attractive if a big part of the codebase was in modern Java (or another modern language) we could kill IDE performances, and this is hard for us to measure 4. TESTABILITY: SonarLint Core has always been intensively tested. Headless testing is much cheaper than IDE UI tests. On the IDE side, it was accepted to have low automated coverage test, relying on manual testing, dogfooding and community to find UI bugs. Now that we put more and more logic in IDEs, where the coverage is low (or absent), there is a higher risk of regressions. 5. CONSISTENCY: We agreed a long time ago to not focus too much on having the same experience in all IDEs. It was considered better to have a "native" experience. Still we are now in a situation where the UX is different between IDEs, with no good reasons. 6. OWNERSHIP of the project. In a situation where there will be one squad per IDE, "consuming" SLCORE, who would be responsible to maintain it (checking failing ITs, updating dependencies, …) and who is allowed to introduce functional changes? Today it is not clear. # Why Not? Possible reasons for not doing anything (or even putting more in IDEs): 1. RIGIDITY: we will force the same behavior in all IDEs. In the past, it has been considered as bad. SonarLint should integrate "natively" is IDEs, which means following IDE "patterns". the configuration and UI will stay under control of the IDE side the part we should put in the core should be the "original" SonarLint experience (analysis, issue tracking, connected mode) so it should not suffer from comparison with existing IDE features 2. SHARED RESPONSIBILITY: having a shared component between multiple teams or squads is difficult in terms of organization. I guess sonar-security or sonar-analyzer-common libraries are in the same situation, we could learn from the LT experience. 3. PERFORMANCE: having SLCORE embedded into SLE/SLI allows to share Java objects/integrate into IDE VFS. By running out of process, we will probably have to duplicate objects in memory. The RPC communication will also be slower than "in-JVM" communication. we should design the API to limit number of RPC calls we should pay attention to memory consumption, but it will be easy to monitor since SonarLint backend will have its own process 4. COMPLEXITY: SLCORE as a library is simpler to use (at least in Java IDEs) than having to manage a separate process 5. INCREASING DISTRIBUTION SIZE: if we package our own runtime # What? 1. API STABILITY: clear interface used by IDEs. Avoid breaking changes, document changes, deprecate/keep backward compatibility. 2. DUPLICATION: move code into SLCORE, but not as a toolbox/library, more as a set of services, with a small and hopefully stable interface. IDEs should ideally only keep UI, settings, and IDE specific integration (extension points, …) 3. RUNTIME: design SLCORE as a standalone process that communicates with IDE using RPC (like SLLS) . The API should support that (no more full Java objects leaking between IDE and Core, avoid too granular/frequent calls). IDE specific code can stay longer with old runtime compatibility. 4. TESTABILITY: we should be able to write a lot of new tests, concerning advanced interactions between components (connected mode storage, VCS, …). For example: user switches its project branch, that should recompute the matching SQ branch, then sync issues, and report to IDE 5. CONSISTENCY: find the right boundaries between IDE specific behavior and common part. Don't allow too much "customization" of the backend to avoid testability hell and no consistency. Document intended IDE specific behavior. 6. OWNERSHIP: if we take inspiration from the SonarQube team organization, we don't think having a dedicated isolated squad owning SLCORE is a good solution. We think most of the functional changes should be driven by IDEs. We will form squads composed of IDE specialists (= frontend) + SLCORE specialists (= backend). ## What should stay in each IDE? IDEs should keep control on the configuration: * connection definition * bindings * exclusion * rules configuration * user preferences (subscribe to dev notifications, …) This is important, because configuration can be scoped/stored at different levels (user, ide instance, project, module, …) and benefit from IDE specific configuration synchronization. Also some settings should be stored in a secure storage, that is also IDE specific. IDEs are responsible for presentation (issues, flows, rule descriptions, …) Should IDEs keep responsibility of analysis scheduling? Maybe not. IDEs should be responsible for hooking into IDE editors to broadcast source file changes. IDEs should be responsible to collect analysis configuration (Java classpath, .NET version, …) # Target architecture ![Target architecture](img/target-architecture.jpg "Target architecture") ================================================ FILE: docs/decisions/0001-implement-binding-suggestions-in-the-core.md ================================================ # Implement binding suggestions in the core ## Why Following [ADR-0000](0000-move-more-responsibilities-to-the-core.md) decision, at the time of implementing [MMF-2895](https://sonarsource.atlassian.net/browse/MMF-2895), we wondered if we should implement most of the feature in the core. A first implementation was made in SLVSCODE for [MMF-1431](https://sonarsource.atlassian.net/browse/MMF-1431) and the question of having a common implementation was raised. ### Decision Drivers * Same drivers as [ADR-0000](0000-move-more-responsibilities-to-the-core.md) ## What ### Considered Options 1. Implement the feature in SLI * Pros: * quicker * Cons: * again logic duplicated in clients 2. Implement the feature in SLCORE and let it handle as much as possible of the feature * Pros: * no duplicated logic * Cons: * slower as the core will need to access more data (connections, projects) ### Option Retained Even if the development would be slower we decided to go with option 2. We see it as an investment for the future. It also goes in the right direction of having more responsibilities in the core, to benefit all clients. ### Consequences * Consequence 1: To be able to make suggestions the CORE will need to know about connections and about projects * Consequence 2: The suggestion algorithm might differ from the one used in SLVSCODE ## How Some other technical decisions that were made: * Provide a `SonarLintBackend` interface as the entry point for clients. A single entrypoint is easier to use * Expose a `SonarLintClient` interface that should be implemented by clients to access some data that will remain their responsibility. A single entrypoint is easier to implement * The backend is to be used as a library (in-process), running it out-of-process will be part of another effort * The backend relies on `lsp4j`, which prepares the ground for moving out-of-process later. Also, the interfaces are designed to supported asynchronous messaging by returning `CompletableFuture`s Decisions related to the `SonarLintBackend`: * only expose the `ConnectionService` and `ConfigurationService` services. More services could come later but were not needed for this feature * those services are designed to be independent * it is the client's responsibility to store connections' data. It knows better how to store it (e.g. password safe) * the backend does not store on disk data under the client's responsibility. Connections and configurations are only kept in memory * they are backed by repositories, `ConfigurationRepository` and `ConnectionConfigurationRepository` that hold the data in-memory. Repositories can be used to write (usually from services) and read (usually from event handlers) * an `EventBus` has been implemented that let services raise events about things that happened. That also improves decoupling * methods used to communicate with the backend should use `DTO`s to transfer data Decisions related to the `SonarLintClient`: * expose a method to get the `HttpClient`. This wouldn't work by going out-of-process, it is a shortcut taken to not lose time and delay the decision about whose responsibility it is to manage HTTP requests (we let it in clients for now) * it is also the client's responsibility for now to access files of the project. This responsibility might be moved to the CORE at some point. For now clients are expected to implement `findFileByNamesInScope`, taking a list of filenames to find to reduce back and forth * let the client display suggestions as they see fit. To let them choose the better UX, suggestions for all projects are delivered at the same time. This might be more adapted for SLE, where there is a single window for all projects Decisions related to the `ConnectionService`: * An `initialize` method, to know what connections were already previously added by the user. We want to distinguish already created connections and newly added ones: we don't want to trigger the same processing * when adding `ConfigurationScope`s several can be provided at the same time. This lets the backend group some processing, e.g. binding suggestions to notify the client a single time with suggestions for all projects added * we have one method per connection event: `didAddConnection`, `didRemoveConnection` and `didUpdateConnection`. At some point it could become a single replace instead, if it is more practical for clients * SonarQube and SonarCloud connections have been separated in 2 different objects, because they don't have exactly the same data. It implies a bit of effort on the client side to make the right transformation. We can still re-evaluate later Decisions related to the `ConfigurationService`: * no `initialize` method, it does not make sense to register `ConfigurationScope`s that are opened before starting the backend. We suppose we will have the same processing for already opened scopes and new ones * when adding `ConfigurationScope`s several can be provided at the same time in `ConfigurationService.configurationScopesAdded`. This lets the backend group some processing, e.g. binding suggestions to notify the client a single time with suggestions for all projects added Decisions related to the `EventBus`: * events are dispatched and handled asynchronously. We don't want to delay the client while handling them * the bus is backed by a single thread executor service. Only one event can be handled at a given time. We think this will help reduce race conditions, and it is simpler to reason about * as handling an event happens asynchronously, handlers should re-check if the conditions of the event are still valid * if event handlers have long processing they should move to a different thread, by being careful about race conditions ## More Information Those decisions were taken in the context of implementing binding suggestions in SLI and trying to keep other IDEs in mind. The API is young and will very probably continue to evolve and break in the beginning ================================================ FILE: docs/decisions/0002-manage-http-client-in-core.md ================================================ # Manage the HTTPClient in the core ## Why At the moment, the core declares a Java interface for HttpClient, that should be implemented by each client, and passed to several APIs. It is the responsibility of the client side to implement this interface, by choosing an HTTPClient implementation, and configuring it (SSL, proxy, timeouts, credentials, ...). The idea is to move the management of this HTTPClient to the core. ### Decision Drivers * Consistent behavior for all IDEs (timeouts, cancellation, SSL, security ...) * More freedom in the core to do more advanced HTTP operations (SSE, websockets, ...) without having to ask each client to implement new code. * Intensive testing of the HTTPClient for our use cases is easier in SLCORE * The current API is not RPC-friendly (asking a Java client to implement an interface) ## What ### Considered Options * keep HTTPClient on IDE side, with an RPC-friendly protocol between client and core * Pros: * more freedom for each IDE * once CORE will be a separate process, the requests will continue to appear as sent by the IDE (maybe important for firewalls) * possibly simpler to integrate into IDE user experience regarding HTTP configuration (SSL, proxy, ...) * possibility to use an HTTPClient provided by the IDE SDK (but not the case as of today) * Cons: * duplicate code in each IDE * possibly different behavior * need to design an RPC-friendly protocol for core to forward HTTP operations to the client. No easy for persistent connections (SSE, websocket) * the core needs its own implementation of an HTTPClient for its medium tests/ITs * move HTTPClient to the core * Pros: * less code in each IDE * consistent behavior * fewer round-trips between client and core * all core tests (medium tests/ITs) will use the "production" HTTPClient * Cons: * need to design an RPC-friendly protocol for core to get credentials, proxy settings, and SSL certificate validation ### Option Retained We retained "move HTTPClient to the core", mainly for the improved testability, and as it fits better with [ADR-0000](0000-move-more-responsibilities-to-the-core.md) decision. ### Consequences * For SLVSCODE, there should be almost no impact * For SLE, it will change the HTTPClient from OkHttp to Apache HTTPClient, so it could have some side effects * For SLI, this will be the same library, but the integration with IntelliJ SSL and Proxy settings will need to be reworked with care * The HTTPClient interface will be deprecated, and should ultimately disappear ## How New requests will be added to the backend protocol: * `getCredentials` to ask client for credentials of a given connection * `selectProxies` to ask client for configured proxies for a given URL * `getProxyAuthentication` to ask client for proxy credentials * `isServerTrusted` to ask client if a given SSL certificate should be trusted ================================================ FILE: docs/decisions/template.md ================================================ # {short title of solved problem and solution} ## Why {Describe the context and problem statement that require to make the decision, e.g., in free form using two to three sentences or in the form of an illustrative story} ### Decision Drivers * {decision driver 1, e.g., a force, facing concern, …} * {decision driver 2, e.g., a force, facing concern, …} * … ## What ### Considered Options * {title of option 1} * Pros: * item 1 * item 2 * Cons: * item 3 * item 4 * {title of option 2} * … ### Option Retained We retained "{title of option 1}", because {justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best}. ### Consequences {Explain what would be the consequences of the decision} * Consequence 1: ... * Consequence 2: ... ## How {Get into more technical details about the change if needed} ## More Information {Additional evidence/confidence for the option retained here} {Define when and how the decision should be realized and if/when it should be re-visited} {Links to other decisions and resources might here appear as well} ================================================ FILE: its/README.md ================================================ # Run integration tests # Prerequisites * Some integration tests load plugins relying on Node.js, so make sure the latest LTS version is installed and `node` is in the PATH. * SonarQube ITs need Java 17 to run and SonarCloud ITs need Java 11 # First time configuration 1. Make sure your Developer Box is properly setup (see xtranet) 2. Configure Orchestrator settings as described [here](https://github.com/SonarSource/orchestrator#configuration). The Artifactory API key and GitHub token are the only mandatory options. The GH token is a Personal Access Token (classic) with the `repo` scope permission, and SSO properly configured 3. For SonarCloud ITs, make sure the `SONARCLOUD_IT_TOKEN` env var is defined (you can find the value in our password management tool) 4. Run `mvn clean install` a first time from this `its` folder so that test resources are built (like custom plugins) # Running ITs from IntelliJ 1. From the root folder of the repository, first build sonarlint-core with `mvn clean install` 2. Make sure the `its` profile is [activated in the Maven tool window](https://www.jetbrains.com/help/idea/work-with-maven-profiles.html#activate_maven_profiles) 3. Reload the project with the top left button of the Maven tool window 4. Open a test class and run it # Running ITs from command line 1. From the root folder of the repository, first build sonarlint-core with `mvn clean install` 2. Run `mvn verify -f its/pom.xml -Dsonar.runtimeVersion=` ================================================ FILE: its/plugins/custom-sensor-plugin/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-its 11.2-SNAPSHOT ../../pom.xml org.sonarsource.plugins.example custom-sensor-plugin sonar-plugin 11.2-SNAPSHOT Example Plugin for SonarQube Example of plugin for SonarQube UTF-8 9.14.0.375 9.9.0.65466 17 org.sonarsource.api.plugin sonar-plugin-api ${sonar.apiVersion} provided org.apache.commons commons-lang3 3.20.0 org.sonarsource.sonarqube sonar-testing-harness ${sonar.testing-harness} test junit junit 4.13.2 test ${project.artifactId} org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin 1.17 true org.sonarsource.plugins.example.ExamplePlugin true org.codehaus.mojo native2ascii-maven-plugin 1.0-beta-1 native2ascii ================================================ FILE: its/plugins/custom-sensor-plugin/src/main/java/org/sonarsource/plugins/example/ExamplePlugin.java ================================================ /* * Example Plugin for SonarQube * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.Plugin; /** * This class is the entry point for all extensions. It is referenced in pom.xml. */ public class ExamplePlugin implements Plugin { @Override public void define(Context context) { context.addExtensions(FooLintRulesDefinition.class, OneIssuePerLineSensor.class); } } ================================================ FILE: its/plugins/custom-sensor-plugin/src/main/java/org/sonarsource/plugins/example/FooLintRulesDefinition.java ================================================ /* * Example Plugin for SonarQube * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import java.io.InputStream; import java.nio.charset.StandardCharsets; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.server.rule.RulesDefinitionXmlLoader; public final class FooLintRulesDefinition implements RulesDefinition { private static final String PATH_TO_RULES_XML = "/example/foolint-rules.xml"; static final String KEY = "foolint"; private static final String NAME = "FooLint"; private final RulesDefinitionXmlLoader xmlLoader; public FooLintRulesDefinition(RulesDefinitionXmlLoader xmlLoader) { this.xmlLoader = xmlLoader; } private String rulesDefinitionFilePath() { return PATH_TO_RULES_XML; } @Override public void define(Context context) { NewRepository repository = context.createRepository(KEY, "java").setName(NAME); InputStream rulesXml = this.getClass().getResourceAsStream(rulesDefinitionFilePath()); if (rulesXml != null) { xmlLoader.load(repository, rulesXml, StandardCharsets.UTF_8.name()); } repository.done(); } } ================================================ FILE: its/plugins/custom-sensor-plugin/src/main/java/org/sonarsource/plugins/example/OneIssuePerLineSensor.java ================================================ /* * Example Plugin for SonarQube * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.rule.RuleKey; public class OneIssuePerLineSensor implements Sensor { @Override public void describe(final SensorDescriptor descriptor) { descriptor.name("One Issue Per Line"); } @Override public void execute(final SensorContext context) { for (InputFile f : context.fileSystem().inputFiles(context.fileSystem().predicates().all())) { for (int i = 1; i < f.lines(); i++) { NewIssue newIssue = context.newIssue(); newIssue .forRule(RuleKey.of(FooLintRulesDefinition.KEY, "ExampleRule1")) .at(newIssue.newLocation().on(f).at(f.selectLine(i)).message("Issue at line " + i)) .save(); } } } } ================================================ FILE: its/plugins/custom-sensor-plugin/src/main/resources/example/foolint-rules.xml ================================================ ExampleRule1 Example Rule 1 ExampleRule1 This is an example of rule defined thru the XML. BLOCKER SINGLE READY example bug CONSTANT_ISSUE 2min ================================================ FILE: its/plugins/global-extension-plugin/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-its 11.2-SNAPSHOT ../../pom.xml org.sonarsource.plugins.example global-extension-plugin sonar-plugin Example Plugin with global extension Example Plugin with global extension UTF-8 9.14.0.375 17 org.sonarsource.api.plugin sonar-plugin-api ${sonar.apiVersion} provided org.slf4j slf4j-api 1.7.36 org.apache.commons commons-lang3 3.20.0 ${project.artifactId} org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin true org.sonarsource.plugins.example.GlobalExtensionPlugin cobol true true ${sonar.apiVersion} ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalExtension.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.slf4j.LoggerFactory; import org.sonar.api.Startable; import org.sonar.api.config.Configuration; import org.sonarsource.api.sonarlint.SonarLintSide; import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES; @SonarLintSide(lifespan = MULTIPLE_ANALYSES) public class GlobalExtension implements Startable { public static GlobalExtension getInstance() { return instance; } private static final org.sonar.api.utils.log.Logger SONAR_API_LOG = org.sonar.api.utils.log.Loggers.get(GlobalExtension.class); private static final org.slf4j.Logger SLF4J_LOG = LoggerFactory.getLogger(GlobalExtension.class); private static GlobalExtension instance; private int counter; private final Configuration config; public GlobalExtension(Configuration config) { instance = this; this.config = config; } @Override public void start() { SONAR_API_LOG.info("Start Global Extension " + config.get("sonar.global.label").orElse("MISSING")); } @Override public void stop() { SLF4J_LOG.info("Stop Global Extension"); } public int getAndInc() { return counter++; } } ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalExtensionPlugin.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.Plugin; public class GlobalExtensionPlugin implements Plugin { @Override public void define(Context context) { context.addExtensions(GlobalLanguage.class, GlobalRulesDefinition.class, GlobalSonarWayProfile.class, GlobalSensor.class, GlobalExtension.class); } } ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalLanguage.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.resources.AbstractLanguage; public class GlobalLanguage extends AbstractLanguage { // Need to use a key that is available in the Language enum static final String LANGUAGE_KEY = "cobol"; public GlobalLanguage() { super(LANGUAGE_KEY, "Global"); } @Override public String[] getFileSuffixes() { return new String[] {"glob"}; } } ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalRulesDefinition.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.server.rule.RuleParamType; import org.sonar.api.server.rule.RulesDefinition; public final class GlobalRulesDefinition implements RulesDefinition { static final String RULE_KEY = "inc"; static final String KEY = "global"; static final String NAME = "Global"; @Override public void define(Context context) { NewRepository repository = context.createRepository(KEY, GlobalLanguage.LANGUAGE_KEY).setName(NAME); NewRule rule = repository.createRule(RULE_KEY) .setActivatedByDefault(true) .setName("Increment") .setHtmlDescription("Increment message after every analysis"); rule.createParam("stringParam") .setType(RuleParamType.STRING) .setName("String parameter") .setDescription("An example of string parameter"); rule.createParam("textParam") .setType(RuleParamType.TEXT) .setDefaultValue("text\nparameter") .setName("Text parameter") .setDescription("An example of text parameter"); rule.createParam("intParam") .setType(RuleParamType.INTEGER) .setDefaultValue("42") .setName("Int parameter") .setDescription("An example of int parameter"); rule.createParam("boolParam") .setType(RuleParamType.BOOLEAN) .setDefaultValue("true") .setName("Boolean parameter") .setDescription("An example boolean parameter"); rule.createParam("floatParam") .setType(RuleParamType.FLOAT) .setDefaultValue("3.14159265358") .setName("Float parameter") .setDescription("An example float parameter"); rule.createParam("enumParam") .setType(RuleParamType.singleListOfValues("enum1", "enum2", "enum3")) .setDefaultValue("enum1") .setName("Enum parameter") .setDescription("An example enum parameter"); rule.createParam("enumListParam") .setType(RuleParamType.multipleListOfValues("list1", "list2", "list3")) .setDefaultValue("list1,list2") .setName("Enum list parameter") .setDescription("An example enum list parameter"); rule.createParam("multipleIntegersParam") .setType(RuleParamType.parse("INTEGER,multiple=true,values=\"80,120,160\"")) .setName("Enum list of integers") .setDescription("An example enum list of integers"); repository.done(); } } ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalSensor.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import java.time.Clock; import java.util.Arrays; import java.util.stream.Stream; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.sensor.Sensor; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; public class GlobalSensor implements Sensor { private static final Logger LOGGER = Loggers.get(GlobalSensor.class); private final Clock clock; public GlobalSensor(Clock clock) { this.clock = clock; } @Override public void describe(final SensorDescriptor descriptor) { descriptor.name("Global") .onlyOnLanguage(GlobalLanguage.LANGUAGE_KEY); } @Override public void execute(final SensorContext context) { long timeBefore = clock.millis(); RuleKey globalRuleKey = RuleKey.of(GlobalRulesDefinition.KEY, GlobalRulesDefinition.RULE_KEY); ActiveRule activeGlobalRule = context.activeRules().find(globalRuleKey); if (activeGlobalRule != null) { Stream.of("stringParam", "textParam", "intParam", "boolParam", "floatParam", "enumParam", "enumListParam", "multipleIntegersParam") .map(k -> Arrays.asList(k, activeGlobalRule.param(k))) .forEach(kv -> LOGGER.info("Param {} has value {}", kv.get(0), kv.get(1))); } else { LOGGER.error("Rule is not active"); } var inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all()); if (!inputFiles.iterator().hasNext()) { LOGGER.error("File system is empty"); } else { for (InputFile f : inputFiles) { NewIssue newIssue = context.newIssue(); newIssue .forRule(globalRuleKey) .at(newIssue.newLocation().on(f).message("Issue number " + GlobalExtension.getInstance().getAndInc())) .save(); } } long timeAfter = clock.millis(); LOGGER.info(String.format("Executed Global Sensor in %d ms", timeAfter - timeBefore)); } } ================================================ FILE: its/plugins/global-extension-plugin/src/main/java/org/sonarsource/plugins/example/GlobalSonarWayProfile.java ================================================ /* * Example Plugin with global extension * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarsource.plugins.example; import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; public final class GlobalSonarWayProfile implements BuiltInQualityProfilesDefinition { @Override public void define(Context context) { context.createBuiltInQualityProfile("Sonar Way", GlobalLanguage.LANGUAGE_KEY) .setDefault(true) .done(); } } ================================================ FILE: its/plugins/java-custom-rules/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-its 11.2-SNAPSHOT ../../pom.xml com.sonarsource.it.java java-custom-rules-plugin sonar-plugin Java Custom Rules Plugin Java Custom Rules 7.16.0.30901 2.1.0.1111 9.14.0.375 17 org.sonarsource.api.plugin sonar-plugin-api ${sonar.apiVersion} provided org.sonarsource.java sonar-java-plugin sonar-plugin ${sonarjava.version} provided org.sonarsource.analyzer-commons sonar-analyzer-commons ${analyzer.commons.version} junit junit 4.13.2 test ${project.artifactId} org.apache.maven.plugins maven-shade-plugin shade package shade org.sonarsource.sonar-packaging-maven-plugin sonar-packaging-maven-plugin 1.23.0.740 true org.sonar.samples.java.MyJavaRulesPlugin ${sonar.apiVersion} custom true true java:${sonarjava.version} java ================================================ FILE: its/plugins/java-custom-rules/src/main/java/org/sonar/samples/java/MyJavaFileCheckRegistrar.java ================================================ /* * Java Custom Rules Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.samples.java; import java.util.List; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonarsource.api.sonarlint.SonarLintSide; /** * Provide the "checks" (implementations of rules) classes that are going be executed during * source code analysis. * * This class is a batch extension by implementing the {@link org.sonar.plugins.java.api.CheckRegistrar} interface. */ @SonarLintSide public class MyJavaFileCheckRegistrar implements CheckRegistrar { /** * Register the classes that will be used to instantiate checks during analysis. */ @Override public void register(RegistrarContext registrarContext) { // Call to registerClassesForRepository to associate the classes with the correct repository key registrarContext.registerClassesForRepository(MyJavaRulesDefinition.REPOSITORY_KEY, checkClasses(), testCheckClasses()); } /** * Lists all the main checks provided by the plugin */ public static List> checkClasses() { return RulesList.getJavaChecks(); } /** * Lists all the test checks provided by the plugin */ public static List> testCheckClasses() { return RulesList.getJavaTestChecks(); } } ================================================ FILE: its/plugins/java-custom-rules/src/main/java/org/sonar/samples/java/MyJavaRulesDefinition.java ================================================ /* * Java Custom Rules Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.samples.java; import java.util.ArrayList; import java.util.Collections; import java.util.Objects; import java.util.Set; import org.sonar.api.SonarEdition; import org.sonar.api.SonarProduct; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarRuntime; import org.sonar.api.server.rule.RulesDefinition; import org.sonar.api.utils.Version; import org.sonarsource.analyzer.commons.RuleMetadataLoader; /** * Declare rule metadata in server repository of rules. * That allows to list the rules in the page "Rules". */ public class MyJavaRulesDefinition implements RulesDefinition { // don't change that because the path is hard coded in CheckVerifier private static final String RESOURCE_BASE_PATH = "org/sonar/l10n/java/rules/java"; public static final String REPOSITORY_KEY = "mycompany-java"; // Add the rule keys of the rules which need to be considered as template-rules private static final Set RULE_TEMPLATES_KEY = Collections.emptySet(); @Override public void define(Context context) { NewRepository repository = context.createRepository(REPOSITORY_KEY, "java").setName("MyCompany Custom Repository"); // The runtime version shouldn't matter. The analyzer is supposed to use it to change behavior based on its runtime version. // But normally, they don't do it in the SonarLint context. RuleMetadataLoader ruleMetadataLoader = new RuleMetadataLoader(RESOURCE_BASE_PATH, getSonarLintRuntime(Version.parse("10.3.0"))); ruleMetadataLoader.addRulesByAnnotatedClass(repository, new ArrayList<>(RulesList.getChecks())); setTemplates(repository); repository.createRule("markdown") .setName("A rule with Markdown description") .setMarkdownDescription(" = Title\n * one\n* two"); repository.done(); } private static SonarRuntime getSonarLintRuntime(Version version) { return new SonarRuntime() { @Override public Version getApiVersion() { return version; } @Override public SonarProduct getProduct() { return SonarProduct.SONARLINT; } @Override public SonarQubeSide getSonarQubeSide() { return null; } @Override public SonarEdition getEdition() { return null; } }; } private static void setTemplates(NewRepository repository) { RULE_TEMPLATES_KEY.stream() .map(repository::rule) .filter(Objects::nonNull) .forEach(rule -> rule.setTemplate(true)); } } ================================================ FILE: its/plugins/java-custom-rules/src/main/java/org/sonar/samples/java/MyJavaRulesPlugin.java ================================================ /* * Java Custom Rules Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.samples.java; import org.sonar.api.Plugin; /** * Entry point of your plugin containing your custom rules */ public class MyJavaRulesPlugin implements Plugin { @Override public void define(Context context) { // server extensions -> objects are instantiated during server startup context.addExtension(MyJavaRulesDefinition.class); // batch extensions -> objects are instantiated during code analysis context.addExtension(MyJavaFileCheckRegistrar.class); } } ================================================ FILE: its/plugins/java-custom-rules/src/main/java/org/sonar/samples/java/RulesList.java ================================================ /* * Java Custom Rules Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.samples.java; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.sonar.plugins.java.api.JavaCheck; import org.sonar.samples.java.checks.AvoidAnnotationRule; public final class RulesList { private RulesList() { } public static List> getChecks() { List> checks = new ArrayList<>(); checks.addAll(getJavaChecks()); checks.addAll(getJavaTestChecks()); return Collections.unmodifiableList(checks); } /** * These rules are going to target MAIN code only */ public static List> getJavaChecks() { return Collections.unmodifiableList(List.of( AvoidAnnotationRule.class)); } /** * These rules are going to target TEST code only */ public static List> getJavaTestChecks() { return Collections.unmodifiableList(List.of()); } } ================================================ FILE: its/plugins/java-custom-rules/src/main/java/org/sonar/samples/java/checks/AvoidAnnotationRule.java ================================================ /* * Java Custom Rules Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.samples.java.checks; import java.util.List; import org.sonar.check.Rule; import org.sonar.check.RuleProperty; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.tree.AnnotationTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TypeTree; @Rule(key = "AvoidAnnotation") public class AvoidAnnotationRule extends BaseTreeVisitor implements JavaFileScanner { private static final String DEFAULT_VALUE = "SuppressWarnings"; private JavaFileScannerContext context; /** * Name of the annotation to avoid. Value can be set by users in Quality profiles. * The key */ @RuleProperty( defaultValue = DEFAULT_VALUE, description = "Name of the annotation to avoid, without the prefix @, for instance 'Override'") protected String name; @Override public void scanFile(JavaFileScannerContext context) { this.context = context; scan(context.getTree()); } @Override public void visitMethod(MethodTree tree) { List annotations = tree.modifiers().annotations(); for (AnnotationTree annotationTree : annotations) { TypeTree annotationType = annotationTree.annotationType(); if (annotationType.is(Tree.Kind.IDENTIFIER)) { IdentifierTree identifier = (IdentifierTree) annotationType; if (identifier.name().equals(name)) { context.reportIssue(this, identifier, String.format("Avoid using annotation @%s", name)); } } } // The call to the super implementation allows to continue the visit of the AST. // Be careful to always call this method to visit every node of the tree. super.visitMethod(tree); } } ================================================ FILE: its/plugins/java-custom-rules/src/main/resources/org/sonar/l10n/java/rules/java/AvoidAnnotation.html ================================================

This rule detects usage of configured annotation

Noncompliant Code Example

TO DO 

Compliant Solution

TO DO 
================================================ FILE: its/plugins/java-custom-rules/src/main/resources/org/sonar/l10n/java/rules/java/AvoidAnnotation.json ================================================ { "title": "Title of AvoidAnnotation", "type": "CODE_SMELL", "status": "ready", "remediation": { "func": "Constant\/Issue", "constantCost": "5min" }, "tags": [ "pitfall" ], "defaultSeverity": "Minor" } ================================================ FILE: its/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-parent 11.2-SNAPSHOT sonarlint-core-its SonarLint Core - ITs Integration tests parent pom plugins/custom-sensor-plugin plugins/java-custom-rules plugins/global-extension-plugin tests true ================================================ FILE: its/tests/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sonarlint-core-its 11.2-SNAPSHOT sonarlint-core-its-tests SonarLint Core - ITs - Tests Integration tests org.sonarsource.sonarlint.core sonarlint-analysis-engine ${project.version} test org.sonarsource.sonarlint.core sonarlint-commons ${project.version} test org.sonarsource.sonarlint.core sonarlint-core ${project.version} test org.sonarsource.sonarlint.core sonarlint-http ${project.version} test org.sonarsource.sonarlint.core sonarlint-plugin-commons ${project.version} test org.sonarsource.sonarlint.core sonarlint-plugin-api ${project.version} test org.sonarsource.sonarlint.core sonarlint-rpc-impl ${project.version} test org.sonarsource.sonarlint.core sonarlint-rule-extractor ${project.version} test org.sonarsource.sonarlint.core sonarlint-server-api ${project.version} test org.sonarsource.sonarlint.core sonarlint-server-connection ${project.version} test org.sonarsource.sonarlint.core sonarlint-telemetry ${project.version} test org.sonarsource.sonarlint.core sonarlint-rpc-java-client ${project.version} test org.sonarsource.sonarqube sonar-ws 9.9.9.104369 test org.sonarsource.api.plugin sonar-plugin-api org.sonarsource.orchestrator sonar-orchestrator-junit5 6.0.3.3907 test org.assertj assertj-core test org.mockito mockito-core test org.junit.jupiter junit-jupiter-engine test org.eclipse.jetty jetty-server 11.0.25 test com.google.guava guava 32.1.1-jre test org.awaitility awaitility test org.apache.commons commons-lang3 test org.apache.commons commons-compress test com.squareup.okhttp3 okhttp-jvm ${okhttp.version} test org.apache.maven.plugins maven-dependency-plugin copy-plugins validate copy com.sonarsource.cpp sonar-cfamily-plugin 6.75.1.93101 jar org.sonarsource.go sonar-go-plugin 1.31.0.4938 jar org.sonarsource.iac sonar-iac-plugin 2.2.0.18377 jar org.sonarsource.javascript sonar-javascript-plugin 11.7.1.36988 jar ${project.build.directory}/plugins false true false ================================================ FILE: its/tests/projects/sample-apex/src/file.cls ================================================ public class Foo { static void MyFooMethod() { Integer target = -5; Integer num = 3; target =- num; // Noncompliant; target = -3. Is that really what's meant? } } ================================================ FILE: its/tests/projects/sample-c/src/file.c ================================================ void end_of_preamble(); #import "foo.h" // Noncompliant int function3(char* ptr) /* Noncompliant; two explicit returns */ { if (ptr == NULL) return -1; return 7; } ================================================ FILE: its/tests/projects/sample-c/src/foo.h ================================================ ================================================ FILE: its/tests/projects/sample-cloudformation/src/sample.yaml ================================================ AWSTemplateFormatVersion: 2010-09-09 Resources: S3Bucket: Type: 'AWS::S3::Bucket' Properties: BucketName: "mybucketname" Tags: - Key: "anycompany:cost-center" # Noncompliant Value: "Accounting" ================================================ FILE: its/tests/projects/sample-cobol/copybooks/Attr.cpy ================================================ 01 ATTRIBUTE-DEFINITIONS. * 05 ATTR-UNPROT PIC X VALUE ' '. 05 ATTR-UNPROT-MDT PIC X VALUE X'C1'. 05 ATTR-UNPROT-BRT PIC X VALUE X'C8'. 05 ATTR-UNPROT-BRT-MDT PIC X VALUE X'C9'. 05 ATTR-UNPROT-DARK PIC X VALUE X'4C'. 05 ATTR-UNPROT-DARK-MDT PIC X VALUE X'4D'. 05 ATTR-UNPROT-NUM PIC X VALUE X'50'. 05 ATTR-UNPROT-NUM-MDT PIC X VALUE X'D1'. 05 ATTR-UNPROT-NUM-BRT PIC X VALUE X'D8'. 05 ATTR-UNPROT-NUM-BRT-MDT PIC X VALUE X'D9'. 05 ATTR-UNPROT-NUM-DARK PIC X VALUE X'5C'. 05 ATTR-UNPROT-NUM-DARK-MDT PIC X VALUE X'5D'. 05 ATTR-PROT PIC X VALUE X'60'. 05 ATTR-PROT-MDT PIC X VALUE X'61'. 05 ATTR-PROT-BRT PIC X VALUE X'E8'. 05 ATTR-PROT-BRT-MDT PIC X VALUE X'E9'. 05 ATTR-PROT-DARK PIC X VALUE '%'. 05 ATTR-PROT-DARK-MDT PIC X VALUE X'6D'. 05 ATTR-PROT-SKIP PIC X VALUE X'F0'. 05 ATTR-PROT-SKIP-MDT PIC X VALUE X'F1'. 05 ATTR-PROT-SKIP-BRT PIC X VALUE X'F8'. 05 ATTR-PROT-SKIP-BRT-MDT PIC X VALUE X'F9'. 05 ATTR-PROT-SKIP-DARK PIC X VALUE X'7C'. 05 ATTR-PROT-SKIP-DARK-MDT PIC X VALUE X'7D'. * 05 ATTR-NO-HIGHLIGHT PIC X VALUE X'00'. 05 ATTR-BLINK PIC X VALUE '1'. 05 ATTR-REVERSE PIC X VALUE '2'. 05 ATTR-UNDERSCORE PIC X VALUE '4'. * 05 ATTR-DEFAULT-COLOR PIC X VALUE X'00'. 05 ATTR-BLUE PIC X VALUE '1'. 05 ATTR-RED PIC X VALUE '2'. 05 ATTR-PINK PIC X VALUE '3'. 05 ATTR-GREEN PIC X VALUE '4'. 05 ATTR-TURQUOISE PIC X VALUE '5'. 05 ATTR-YELLOW PIC X VALUE '6'. 05 ATTR-NEUTRAL PIC X VALUE '7'. ================================================ FILE: its/tests/projects/sample-cobol/copybooks/Custmas.cpy ================================================ 01 CUSTOMER-MASTER-RECORD. * 05 CM-CUSTOMER-NUMBER PIC X(6). 05 CM-FIRST-NAME PIC X(20). 05 CM-LAST-NAME PIC X(30). 05 CM-ADDRESS PIC X(30). 05 CM-CITY PIC X(20). 05 CM-STATE PIC X(2). 05 CM-ZIP-CODE PIC X(10). ================================================ FILE: its/tests/projects/sample-cobol/copybooks/Errparm.cpy ================================================ 01 ERROR-PARAMETERS. * 05 ERR-RESP PIC S9(8) COMP. 05 ERR-RESP2 PIC S9(8) COMP. 05 ERR-TRNID PIC X(4) VALUE IS 99. 05 ERR-RSRCE PIC X(8). ================================================ FILE: its/tests/projects/sample-cobol/copybooks/MNTSET2.CPY ================================================ * Micro Focus BMS Screen Painter (ver MFBM 2.0.11) * MapSet Name MNTSET2 * Date Created 04/16/2001 * Time Created 14:38:47 * Input Data For Map MNTMAP1 01 MNTMAP1I. 03 FILLER PIC X(12). 03 TRANID1L PIC S9(4) COMP. 03 TRANID1F PIC X. 03 FILLER REDEFINES TRANID1F. 05 TRANID1A PIC X. 03 FILLER PIC X(2). 03 TRANID1I PIC X(4). 03 CUSTNO1L PIC S9(4) COMP. 03 CUSTNO1F PIC X. 03 FILLER REDEFINES CUSTNO1F. 05 CUSTNO1A PIC X. 03 FILLER PIC X(2). 03 CUSTNO1I PIC X(6). 03 ACTIONL PIC S9(4) COMP. 03 ACTIONF PIC X. 03 FILLER REDEFINES ACTIONF. 05 ACTIONA PIC X. 03 FILLER PIC X(2). 03 ACTIONI PIC X(1). 03 MSG1L PIC S9(4) COMP. 03 MSG1F PIC X. 03 FILLER REDEFINES MSG1F. 05 MSG1A PIC X. 03 FILLER PIC X(2). 03 MSG1I PIC X(79). 03 DUMMY1L PIC S9(4) COMP. 03 DUMMY1F PIC X. 03 FILLER REDEFINES DUMMY1F. 05 DUMMY1A PIC X. 03 FILLER PIC X(2). 03 DUMMY1I PIC X(1). * Output Data For Map MNTMAP1 01 MNTMAP1O REDEFINES MNTMAP1I. 03 FILLER PIC X(12). 03 FILLER PIC X(3). 03 TRANID1C PIC X. 03 TRANID1H PIC X. 03 TRANID1O PIC X(4). 03 FILLER PIC X(3). 03 CUSTNO1C PIC X. 03 CUSTNO1H PIC X. 03 CUSTNO1O PIC X(6). 03 FILLER PIC X(3). 03 ACTIONC PIC X. 03 ACTIONH PIC X. 03 ACTIONO PIC X(1). 03 FILLER PIC X(3). 03 MSG1C PIC X. 03 MSG1H PIC X. 03 MSG1O PIC X(79). 03 FILLER PIC X(3). 03 DUMMY1C PIC X. 03 DUMMY1H PIC X. 03 DUMMY1O PIC X(1). * Input Data For Map MNTMAP2 01 MNTMAP2I. 03 FILLER PIC X(12). 03 TRANID2L PIC S9(4) COMP. 03 TRANID2F PIC X. 03 FILLER REDEFINES TRANID2F. 05 TRANID2A PIC X. 03 FILLER PIC X(2). 03 TRANID2I PIC X(4). 03 INSTR2L PIC S9(4) COMP. 03 INSTR2F PIC X. 03 FILLER REDEFINES INSTR2F. 05 INSTR2A PIC X. 03 FILLER PIC X(2). 03 INSTR2I PIC X(79). 03 CUSTNO2L PIC S9(4) COMP. 03 CUSTNO2F PIC X. 03 FILLER REDEFINES CUSTNO2F. 05 CUSTNO2A PIC X. 03 FILLER PIC X(2). 03 CUSTNO2I PIC X(6). 03 LNAMEL PIC S9(4) COMP. 03 LNAMEF PIC X. 03 FILLER REDEFINES LNAMEF. 05 LNAMEA PIC X. 03 FILLER PIC X(2). 03 LNAMEI PIC X(30). 03 FNAMEL PIC S9(4) COMP. 03 FNAMEF PIC X. 03 FILLER REDEFINES FNAMEF. 05 FNAMEA PIC X. 03 FILLER PIC X(2). 03 FNAMEI PIC X(20). 03 ADDRL PIC S9(4) COMP. 03 ADDRF PIC X. 03 FILLER REDEFINES ADDRF. 05 ADDRA PIC X. 03 FILLER PIC X(2). 03 ADDRI PIC X(30). 03 CITYL PIC S9(4) COMP. 03 CITYF PIC X. 03 FILLER REDEFINES CITYF. 05 CITYA PIC X. 03 FILLER PIC X(2). 03 CITYI PIC X(20). 03 STATEL PIC S9(4) COMP. 03 STATEF PIC X. 03 FILLER REDEFINES STATEF. 05 STATEA PIC X. 03 FILLER PIC X(2). 03 STATEI PIC X(2). 03 ZIPCODEL PIC S9(4) COMP. 03 ZIPCODEF PIC X. 03 FILLER REDEFINES ZIPCODEF. 05 ZIPCODEA PIC X. 03 FILLER PIC X(2). 03 ZIPCODEI PIC X(10). 03 MSG2L PIC S9(4) COMP. 03 MSG2F PIC X. 03 FILLER REDEFINES MSG2F. 05 MSG2A PIC X. 03 FILLER PIC X(2). 03 MSG2I PIC X(79). 03 DUMMY2L PIC S9(4) COMP. 03 DUMMY2F PIC X. 03 FILLER REDEFINES DUMMY2F. 05 DUMMY2A PIC X. 03 FILLER PIC X(2). 03 DUMMY2I PIC X(1). * Output Data For Map MNTMAP2 01 MNTMAP2O REDEFINES MNTMAP2I. 03 FILLER PIC X(12). 03 FILLER PIC X(3). 03 TRANID2C PIC X. 03 TRANID2H PIC X. 03 TRANID2O PIC X(4). 03 FILLER PIC X(3). 03 INSTR2C PIC X. 03 INSTR2H PIC X. 03 INSTR2O PIC X(79). 03 FILLER PIC X(3). 03 CUSTNO2C PIC X. 03 CUSTNO2H PIC X. 03 CUSTNO2O PIC X(6). 03 FILLER PIC X(3). 03 LNAMEC PIC X. 03 LNAMEH PIC X. 03 LNAMEO PIC X(30). 03 FILLER PIC X(3). 03 FNAMEC PIC X. 03 FNAMEH PIC X. 03 FNAMEO PIC X(20). 03 FILLER PIC X(3). 03 ADDRC PIC X. 03 ADDRH PIC X. 03 ADDRO PIC X(30). 03 FILLER PIC X(3). 03 CITYC PIC X. 03 CITYH PIC X. 03 CITYO PIC X(20). 03 FILLER PIC X(3). 03 STATEC PIC X. 03 STATEH PIC X. 03 STATEO PIC X(2). 03 FILLER PIC X(3). 03 ZIPCODEC PIC X. 03 ZIPCODEH PIC X. 03 ZIPCODEO PIC X(10). 03 FILLER PIC X(3). 03 MSG2C PIC X. 03 MSG2H PIC X. 03 MSG2O PIC X(79). 03 FILLER PIC X(3). 03 DUMMY2C PIC X. 03 DUMMY2H PIC X. 03 DUMMY2O PIC X(1). ================================================ FILE: its/tests/projects/sample-cobol/src/Custmnt2.cbl ================================================ IDENTIFICATION DIVISION. * PROGRAM-ID. CUSTMNT2. * ENVIRONMENT DIVISION. * DATA DIVISION. * WORKING-STORAGE SECTION. * 01 SWITCHES. * 05 VALID-DATA-SW PIC X(01) VALUE 'Y'. 88 VALID-DATA VALUE 'Y'. * 01 FLAGS. * 05 SEND-FLAG PIC X(01). 88 SEND-ERASE VALUE '1'. 88 SEND-ERASE-ALARM VALUE '2'. 88 SEND-DATAONLY VALUE '3'. 88 SEND-DATAONLY-ALARM VALUE '4'. * 01 WORK-FIELDS. * 05 RESPONSE-CODE PIC S9(08) COMP. * 01 USER-INSTRUCTIONS. * 05 ADD-INSTRUCTION PIC X(79) VALUE 'Type information for new customer. Then Press Enter.'. 05 CHANGE-INSTRUCTION PIC X(79) VALUE 'Type changes. Then press Enter.'. 05 DELETE-INSTRUCTION PIC X(79) VALUE 'Press Enter to delete this customer or press F12 to canc - 'el.'. * 01 COMMUNICATION-AREA. * 05 CA-CONTEXT-FLAG PIC X(01). 88 PROCESS-KEY-MAP VALUE '1'. 88 PROCESS-ADD-CUSTOMER VALUE '2'. 88 PROCESS-CHANGE-CUSTOMER VALUE '3'. 88 PROCESS-DELETE-CUSTOMER VALUE '4'. 05 CA-CUSTOMER-RECORD. 10 CA-CUSTOMER-NUMBER PIC X(06). 10 FILLER PIC X(112). * COPY CUSTMAS. * COPY MNTSET2. * COPY DFHAID. * COPY ATTR. * COPY ERRPARM. * LINKAGE SECTION. * 01 DFHCOMMAREA PIC X(119). * PROCEDURE DIVISION. * 0000-PROCESS-CUSTOMER-MAINT. * IF EIBCALEN > ZERO MOVE DFHCOMMAREA TO COMMUNICATION-AREA END-IF. * EVALUATE TRUE * WHEN EIBCALEN = ZERO MOVE LOW-VALUE TO MNTMAP1O MOVE -1 TO CUSTNO1L SET SEND-ERASE TO TRUE PERFORM 1500-SEND-KEY-MAP SET PROCESS-KEY-MAP TO TRUE * WHEN EIBAID = DFHPF3 EXEC CICS XCTL PROGRAM('INVMENU') END-EXEC * WHEN EIBAID = DFHPF12 IF PROCESS-KEY-MAP EXEC CICS XCTL PROGRAM('INVMENU') END-EXEC ELSE MOVE LOW-VALUE TO MNTMAP1O MOVE -1 TO CUSTNO1L SET SEND-ERASE TO TRUE PERFORM 1500-SEND-KEY-MAP SET PROCESS-KEY-MAP TO TRUE END-IF * WHEN EIBAID = DFHCLEAR IF PROCESS-KEY-MAP MOVE LOW-VALUE TO MNTMAP1O MOVE -1 TO CUSTNO1L SET SEND-ERASE TO TRUE PERFORM 1500-SEND-KEY-MAP ELSE MOVE LOW-VALUE TO MNTMAP2O MOVE CA-CUSTOMER-NUMBER TO CUSTNO2O EVALUATE TRUE WHEN PROCESS-ADD-CUSTOMER MOVE ADD-INSTRUCTION TO INSTR2O WHEN PROCESS-CHANGE-CUSTOMER MOVE CHANGE-INSTRUCTION TO INSTR2O WHEN PROCESS-DELETE-CUSTOMER MOVE DELETE-INSTRUCTION TO INSTR2O END-EVALUATE MOVE -1 TO LNAMEL SET SEND-ERASE TO TRUE PERFORM 1400-SEND-DATA-MAP END-IF * WHEN EIBAID = DFHPA1 OR DFHPA2 OR DFHPA3 CONTINUE * WHEN EIBAID = DFHENTER EVALUATE TRUE WHEN PROCESS-KEY-MAP PERFORM 1000-PROCESS-KEY-MAP WHEN PROCESS-ADD-CUSTOMER PERFORM 2000-PROCESS-ADD-CUSTOMER WHEN PROCESS-CHANGE-CUSTOMER PERFORM 3000-PROCESS-CHANGE-CUSTOMER WHEN PROCESS-DELETE-CUSTOMER PERFORM 4000-PROCESS-DELETE-CUSTOMER END-EVALUATE * WHEN OTHER IF PROCESS-KEY-MAP MOVE LOW-VALUE TO MNTMAP1O MOVE 'That key is unassigned.' TO MSG1O MOVE -1 TO CUSTNO1L SET SEND-DATAONLY-ALARM TO TRUE PERFORM 1500-SEND-KEY-MAP ELSE MOVE LOW-VALUE TO MNTMAP2O MOVE 'That key is unassigned.' TO MSG2O MOVE -1 TO LNAMEL SET SEND-DATAONLY-ALARM TO TRUE PERFORM 1400-SEND-DATA-MAP END-IF * END-EVALUATE. EXEC CICS RETURN TRANSID('MNT2') COMMAREA(COMMUNICATION-AREA) END-EXEC. * 1000-PROCESS-KEY-MAP. * PERFORM 1100-RECEIVE-KEY-MAP. PERFORM 1200-EDIT-KEY-DATA. IF VALID-DATA IF NOT PROCESS-DELETE-CUSTOMER INSPECT CUSTOMER-MASTER-RECORD REPLACING ALL SPACE BY '_' END-IF MOVE CUSTNO1I TO CUSTNO2O MOVE CM-LAST-NAME TO LNAMEO MOVE CM-FIRST-NAME TO FNAMEO MOVE CM-ADDRESS TO ADDRO MOVE CM-CITY TO CITYO MOVE CM-STATE TO STATEO MOVE CM-ZIP-CODE TO ZIPCODEO MOVE -1 TO LNAMEL SET SEND-ERASE TO TRUE PERFORM 1400-SEND-DATA-MAP ELSE MOVE LOW-VALUE TO CUSTNO1O ACTIONO SET SEND-DATAONLY-ALARM TO TRUE PERFORM 1500-SEND-KEY-MAP END-IF. * 1100-RECEIVE-KEY-MAP. * EXEC CICS RECEIVE MAP('MNTMAP1') MAPSET('MNTSET2') INTO(MNTMAP1I) END-EXEC. * INSPECT MNTMAP1I REPLACING ALL '_' BY SPACE. * 1200-EDIT-KEY-DATA. * MOVE ATTR-NO-HIGHLIGHT TO ACTIONH CUSTNO1H. * IF ACTIONI NOT = '1' AND '2' AND '3' AND '4' AND '5' MOVE ATTR-REVERSE TO ACTIONH MOVE -1 TO ACTIONL MOVE 'Action must be 1, 2, or 3.' TO MSG1O MOVE 'N' TO VALID-DATA-SW END-IF. * IF CUSTNO1L = ZERO OR CUSTNO1I = SPACE MOVE ATTR-REVERSE TO CUSTNO1H MOVE -1 TO CUSTNO1L MOVE 'You must enter a customer number.' TO MSG1O MOVE 'N' TO VALID-DATA-SW END-IF. * IF VALID-DATA MOVE LOW-VALUE TO MNTMAP2O EVALUATE ACTIONI WHEN '1' PERFORM 1300-READ-CUSTOMER-RECORD IF RESPONSE-CODE = DFHRESP(NOTFND) MOVE ADD-INSTRUCTION TO INSTR2O SET PROCESS-ADD-CUSTOMER TO TRUE MOVE SPACE TO CUSTOMER-MASTER-RECORD ELSE IF RESPONSE-CODE = DFHRESP(NORMAL) MOVE 'That customer already exists.' TO MSG1O MOVE 'N' TO VALID-DATA-SW END-IF END-IF WHEN '2' PERFORM 1300-READ-CUSTOMER-RECORD IF RESPONSE-CODE = DFHRESP(NORMAL) MOVE CUSTOMER-MASTER-RECORD TO CA-CUSTOMER-RECORD MOVE CHANGE-INSTRUCTION TO INSTR2O SET PROCESS-CHANGE-CUSTOMER TO TRUE ELSE IF RESPONSE-CODE = DFHRESP(NOTFND) MOVE 'That customer does not exist.' TO MSG1O MOVE 'N' TO VALID-DATA-SW END-IF END-IF WHEN '3' PERFORM 1300-READ-CUSTOMER-RECORD IF RESPONSE-CODE = DFHRESP(NORMAL) MOVE CUSTOMER-MASTER-RECORD TO CA-CUSTOMER-RECORD MOVE DELETE-INSTRUCTION TO INSTR2O SET PROCESS-DELETE-CUSTOMER TO TRUE MOVE ATTR-PROT TO LNAMEA FNAMEA ADDRA CITYA STATEA ZIPCODEA ELSE IF RESPONSE-CODE = DFHRESP(NOTFND) MOVE 'That customer does not exist.' TO MSG1O MOVE 'N' TO VALID-DATA-SW END-IF END-IF END-EVALUATE. * 1300-READ-CUSTOMER-RECORD. * EXEC CICS READ FILE('CUSTMAS') INTO(CUSTOMER-MASTER-RECORD) RIDFLD(CUSTNO1I) RESP(RESPONSE-CODE) END-EXEC. * IF RESPONSE-CODE NOT = DFHRESP(NORMAL) AND RESPONSE-CODE NOT = DFHRESP(NOTFND) PERFORM 9999-TERMINATE-PROGRAM END-IF. * 1400-SEND-DATA-MAP. * MOVE 'MNT2' TO TRANID2O. * EVALUATE TRUE WHEN SEND-ERASE EXEC CICS SEND MAP('MNTMAP2') MAPSET('MNTSET2') FROM(MNTMAP2O) ERASE CURSOR END-EXEC WHEN SEND-DATAONLY-ALARM EXEC CICS SEND MAP('MNTMAP2') MAPSET('MNTSET2') FROM(MNTMAP2O) DATAONLY ALARM CURSOR END-EXEC END-EVALUATE. * 1500-SEND-KEY-MAP. * MOVE 'MNT2' TO TRANID1O. * EVALUATE TRUE WHEN SEND-ERASE EXEC CICS SEND MAP('MNTMAP1') MAPSET('MNTSET2') FROM(MNTMAP1O) ERASE CURSOR END-EXEC WHEN SEND-ERASE-ALARM EXEC CICS SEND MAP('MNTMAP1') MAPSET('MNTSET2') FROM(MNTMAP1O) ERASE ALARM CURSOR END-EXEC WHEN SEND-DATAONLY-ALARM EXEC CICS SEND MAP('MNTMAP1') MAPSET('MNTSET2') FROM(MNTMAP1O) DATAONLY ALARM CURSOR END-EXEC END-EVALUATE. * 2000-PROCESS-ADD-CUSTOMER. * PERFORM 2100-RECEIVE-DATA-MAP. PERFORM 2200-EDIT-CUSTOMER-DATA. IF VALID-DATA PERFORM 2300-WRITE-CUSTOMER-RECORD IF RESPONSE-CODE = DFHRESP(NORMAL) MOVE 'Customer record added.' TO MSG1O SET SEND-ERASE TO TRUE ELSE IF RESPONSE-CODE = DFHRESP(DUPREC) MOVE 'Another user has added a record with that c - 'ustomer number.' TO MSG1O SET SEND-ERASE-ALARM TO TRUE END-IF END-IF MOVE -1 TO CUSTNO1L PERFORM 1500-SEND-KEY-MAP SET PROCESS-KEY-MAP TO TRUE ELSE MOVE LOW-VALUE TO LNAMEO FNAMEO ADDRO CITYO STATEO ZIPCODEO SET SEND-DATAONLY-ALARM TO TRUE PERFORM 1400-SEND-DATA-MAP END-IF. * 2100-RECEIVE-DATA-MAP. * EXEC CICS RECEIVE MAP('MNTMAP2') MAPSET('MNTSET2') INTO(MNTMAP2I) END-EXEC. * INSPECT MNTMAP2I REPLACING ALL '_' BY SPACE. * 2200-EDIT-CUSTOMER-DATA. * MOVE ATTR-NO-HIGHLIGHT TO ZIPCODEH STATEH CITYH ADDRH FNAMEH LNAMEH. IF ZIPCODEI = SPACE OR ZIPCODEL = ZERO MOVE ATTR-REVERSE TO ZIPCODEH MOVE -1 TO ZIPCODEL MOVE 'You must enter a zip code.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. IF STATEI = SPACE OR STATEL = ZERO MOVE ATTR-REVERSE TO STATEH MOVE -1 TO STATEL MOVE 'You must enter a state.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. IF CITYI = SPACE OR CITYL = ZERO MOVE ATTR-REVERSE TO CITYH MOVE -1 TO CITYL MOVE 'You must enter a city.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. IF ADDRI = SPACE OR ADDRL = ZERO MOVE ATTR-REVERSE TO ADDRH MOVE -1 TO ADDRL MOVE 'You must enter an address.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. IF FNAMEI = SPACE OR FNAMEL = ZERO MOVE ATTR-REVERSE TO FNAMEH MOVE -1 TO FNAMEL MOVE 'You must enter a first name.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. IF LNAMEI = SPACE OR LNAMEL = ZERO MOVE ATTR-REVERSE TO LNAMEH MOVE -1 TO LNAMEL MOVE 'You must enter a last name.' TO MSG2O MOVE 'N' TO VALID-DATA-SW END-IF. * 2300-WRITE-CUSTOMER-RECORD. * MOVE CUSTNO2I TO CM-CUSTOMER-NUMBER. MOVE LNAMEI TO CM-LAST-NAME. MOVE FNAMEI TO CM-FIRST-NAME. MOVE ADDRI TO CM-ADDRESS. MOVE CITYI TO CM-CITY. MOVE STATEI TO CM-STATE. MOVE ZIPCODEI TO CM-ZIP-CODE. * EXEC CICS WRITE FILE('CUSTMAS') FROM(CUSTOMER-MASTER-RECORD) RIDFLD(CM-CUSTOMER-NUMBER) RESP(RESPONSE-CODE) END-EXEC. * IF RESPONSE-CODE NOT = DFHRESP(NORMAL) AND RESPONSE-CODE NOT = DFHRESP(DUPREC) PERFORM 9999-TERMINATE-PROGRAM END-IF. * 3000-PROCESS-CHANGE-CUSTOMER. * PERFORM 2100-RECEIVE-DATA-MAP. PERFORM 2200-EDIT-CUSTOMER-DATA. IF VALID-DATA MOVE CUSTNO2I TO CM-CUSTOMER-NUMBER PERFORM 3100-READ-CUSTOMER-FOR-UPDATE IF RESPONSE-CODE = DFHRESP(NORMAL) IF CUSTOMER-MASTER-RECORD = CA-CUSTOMER-RECORD * Introduce extra nested if as an example of rule violation IF VALID-DATA IF RESPONSE-CODE = DFHRESP(NORMAL) PERFORM 3200-REWRITE-CUSTOMER-RECORD MOVE 'Customer record updated.' TO MSG1O SET SEND-ERASE TO TRUE END-IF END-IF ELSE MOVE 'Another user has updated the record. Try a - 'gain.' TO MSG1O SET SEND-ERASE-ALARM TO TRUE END-IF ELSE IF RESPONSE-CODE = DFHRESP(NOTFND) MOVE 'Another user has deleted the record.' TO MSG1O SET SEND-ERASE-ALARM TO TRUE END-IF END-IF MOVE -1 TO CUSTNO1L PERFORM 1500-SEND-KEY-MAP SET PROCESS-KEY-MAP TO TRUE ELSE MOVE LOW-VALUE TO LNAMEO FNAMEO ADDRO CITYO STATEO ZIPCODEO SET SEND-DATAONLY-ALARM TO TRUE PERFORM 1400-SEND-DATA-MAP END-IF. * 3100-READ-CUSTOMER-FOR-UPDATE. * EXEC CICS READ FILE('CUSTMAS') INTO(CUSTOMER-MASTER-RECORD) RIDFLD(CM-CUSTOMER-NUMBER) UPDATE RESP(RESPONSE-CODE) END-EXEC. * IF RESPONSE-CODE NOT = DFHRESP(NORMAL) AND RESPONSE-CODE NOT = DFHRESP(NOTFND) PERFORM 9999-TERMINATE-PROGRAM END-IF. * 3200-REWRITE-CUSTOMER-RECORD. * MOVE LNAMEI TO CM-LAST-NAME. MOVE FNAMEI TO CM-FIRST-NAME. MOVE ADDRI TO CM-ADDRESS. MOVE CITYI TO CM-CITY. MOVE STATEI TO CM-STATE. MOVE ZIPCODEI TO CM-ZIP-CODE. * EXEC CICS REWRITE FILE('CUSTMAS') FROM(CUSTOMER-MASTER-RECORD) RESP(RESPONSE-CODE) END-EXEC. * IF RESPONSE-CODE NOT = DFHRESP(NORMAL) PERFORM 9999-TERMINATE-PROGRAM END-IF. * 4000-PROCESS-DELETE-CUSTOMER. * MOVE CA-CUSTOMER-NUMBER TO CM-CUSTOMER-NUMBER. PERFORM 3100-READ-CUSTOMER-FOR-UPDATE. IF RESPONSE-CODE = DFHRESP(NORMAL) ALTER X TO PROCEED TO Y IF CUSTOMER-MASTER-RECORD = CA-CUSTOMER-RECORD PERFORM 4100-DELETE-CUSTOMER-RECORD MOVE 'Customer deleted.' TO MSG1O SET SEND-ERASE TO TRUE ELSE MOVE 'Another user has updated the record. Try again - '.' TO MSG1O SET SEND-ERASE-ALARM TO TRUE END-IF ELSE IF RESPONSE-CODE = DFHRESP(NOTFND) MOVE 'Another user has deleted the record.' TO MSG1O SET SEND-ERASE-ALARM TO TRUE END-IF END-IF. MOVE -1 TO CUSTNO1L. PERFORM 1500-SEND-KEY-MAP. SET PROCESS-KEY-MAP TO TRUE. * 4100-DELETE-CUSTOMER-RECORD. * TODO Some comment EXEC CICS DELETE FILE('CUSTMAS') RESP(RESPONSE-CODE) END-EXEC. * IF RESPONSE-CODE NOT = DFHRESP(NORMAL) PERFORM 9999-TERMINATE-PROGRAM END-IF. * 9999-TERMINATE-PROGRAM. * MOVE EIBRESP TO ERR-RESP. MOVE EIBRESP2 TO ERR-RESP2. MOVE EIBTRNID TO ERR-TRNID. MOVE EIBRSRCE TO ERR-RSRCE. * EXEC CICS XCTL PROGRAM('SYSERR') COMMAREA(ERROR-PARAMETERS) END-EXEC. ================================================ FILE: its/tests/projects/sample-custom-secrets/src/file.md ================================================ # README Secret is YaYYaYYaY ================================================ FILE: its/tests/projects/sample-dbd/src/hello.py ================================================ def out_of_bounds_access(): my_list = [] print(my_list[0]) ================================================ FILE: its/tests/projects/sample-docker/src/Dockerfile ================================================ from ubuntu:22.04 as jammy ================================================ FILE: its/tests/projects/sample-global-extension/src/foo.glob ================================================ Foo ================================================ FILE: its/tests/projects/sample-go/src/sample.go ================================================ package main import ( "crypto/rand" "crypto/rsa" "fmt" ) func encrypt(plaintext []byte) []byte { random := rand.Reader privateKey, _ := rsa.GenerateKey(random, 4096) ciphertext, _ := rsa.EncryptPKCS1v15(random, &privateKey.PublicKey, plaintext) return ciphertext } func add(x, y int) int { return x + y z := x + y } ================================================ FILE: its/tests/projects/sample-java/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sample-java 1.0-SNAPSHOT Sample Java Sample project for ITs 17 17 ================================================ FILE: its/tests/projects/sample-java/src/main/java/foo/Foo.java ================================================ package foo; public class Foo { public void call_echo() { echo(3); } public void echo(int i) { should_be_static(); } // invalid private void should_be_static() { System.out.println("Foo"); } } ================================================ FILE: its/tests/projects/sample-java-custom/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sample-java 1.0-SNAPSHOT Sample Java Sample project for ITs 17 17 ================================================ FILE: its/tests/projects/sample-java-custom/src/main/java/foo/Foo.java ================================================ package foo; public class Foo { public void call_echo() { echo(3); } public void echo(int i) { should_be_static(); } @SuppressWarnings("") private void should_be_static() { System.out.println("Foo"); } } ================================================ FILE: its/tests/projects/sample-java-hotspot/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sample-java-hotspot 1.0-SNAPSHOT Sample Java Hotspot Sample project for ITs 17 17 ================================================ FILE: its/tests/projects/sample-java-hotspot/src/main/java/foo/Foo.java ================================================ package foo; import java.util.logging.Level; import java.util.logging.Logger; public class Foo { public static void configureLogging() { Logger.getGlobal().setLevel(Level.FINEST); } } ================================================ FILE: its/tests/projects/sample-java-taint/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sample-java-taint 1.0-SNAPSHOT Sample Java Taint Sample project for ITs javax.servlet javax.servlet-api 3.0.1 provided 17 17 ================================================ FILE: its/tests/projects/sample-java-taint/src/main/java/foo/DbHelper.java ================================================ package foo; import java.sql.SQLException; public class DbHelper { static boolean executeQuery(java.sql.Connection connection, String user, String pass) throws SQLException { String query = "SELECT * FROM users WHERE user = '" + user + "' AND pass = '" + pass + "'"; // Unsafe java.sql.Statement statement = connection.createStatement(); java.sql.ResultSet resultSet = statement.executeQuery(query); // Noncompliant return resultSet.next(); } } ================================================ FILE: its/tests/projects/sample-java-taint/src/main/java/foo/Endpoint.java ================================================ package foo; import java.sql.SQLException; public class Endpoint { public boolean authenticate(javax.servlet.http.HttpServletRequest request, java.sql.Connection connection) throws SQLException { String user = request.getParameter("user"); String pass = request.getParameter("pass"); return DbHelper.executeQuery(connection, user, pass); } } ================================================ FILE: its/tests/projects/sample-javascript/src/Person.js ================================================ var Person = function(first, last, middle) { this.first = first; this.middle = middle; this.last = last; }; Person.prototype = { whoAreYou : function() { fullName = [this.first, this.middle, this.last].filter(x => x).join(' '); return fullName; } }; ================================================ FILE: its/tests/projects/sample-jcl/GAM0VCDB.jcl ================================================ //* Noncompliant@+2 {{Replace this implicit SYSIN DD * statement with an explicit one.}} // this is some value //* ^[sc=1;ec=18] // //* Noncompliant@+2 // implicit dd * with concatenated statement, only this datastream should be highlighted //* ^[sc=1;el=+1;ec=42]@-1 // DD DSN=CONCATENATED-STATEMENT //* //* Noncompliant@+2 //MYDD DD DNS=TEST this is some value //SYSIN DD * some data /* //* //SYSIN DD * DLM=AA some data AA //* //* Noncompliant@+2 //MYJOB JOB some data //* Noncompliant@+2 // CNTL some data // ENDCNTL //* //* Noncompliant@+2 // PROC some data // ENDPROC ================================================ FILE: its/tests/projects/sample-kotlin/src/hello.kt ================================================ fun foo() { val a = 1 a = a } ================================================ FILE: its/tests/projects/sample-kubernetes/src/sample.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: test spec: containers: - image: k8s.gcr.io/test-webserver name: test-container volumeMounts: - mountPath: /data name: test-volume volumes: - name: test-volume hostPath: path: /etc # Sensitive ================================================ FILE: its/tests/projects/sample-language-mix/pom.xml ================================================ 4.0.0 org.sonarsource.sonarlint.core sample-language-mix 1.0-SNAPSHOT Sample Language Mix Sample project for ITs org.apache.maven.plugins maven-compiler-plugin 3.5 1.7 1.7 ================================================ FILE: its/tests/projects/sample-language-mix/src/main/java/foo/Foo.java ================================================ package foo; public class Foo { public void call_echo() { echo(3); } public void echo(int i) { should_be_static(); } // invalid private void should_be_static() { System.out.println("Foo"); } } ================================================ FILE: its/tests/projects/sample-language-mix/src/main/java/foo/main.py ================================================ def my_function(name): print "Hello world!" ================================================ FILE: its/tests/projects/sample-misra/foo.cpp ================================================ extern int * f(); int func() { if ( f == nullptr ) {// Non-compliant } } ================================================ FILE: its/tests/projects/sample-php/src/Math.php ================================================ . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * * Neither the name of Manuel Pichler nor the names of his * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * @package Example * @author Manuel Pichler * @copyright 2007-2009 Manuel Pichler. All rights reserved. * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @version SVN: $Id: Math.php 4429 2009-01-04 15:39:45Z mapi $ * @link http://www.phpundercontrol.org/ */ function add($v1 , $v2) { return ($v1 + $v2); } /** * Simple math class. * * @package Example * @author Manuel Pichler * @copyright 2007-2009 Manuel Pichler. All rights reserved. * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @version Release: 0.5.0 * @link http://www.phpundercontrol.org/ */ class PhpUnderControl_Example_Math { /** * Adds the two given values. * * @param integer $v1 Value one. * @param integer $v2 Value two. * * @return integer. */ public function add($v1 , $v2) { //TODO add something else return ($v1 + $v2); } /** * Subtract param two from param one * * @param integer $v1 Value one. * @param integer $v2 Value two. * * @return integer. */ public function sub($v1, $v2) { return $v1 - $v2; } /** * Not tested method that should be visible with low coverage. */ public function div($v1, $v2) { $v3 = $v1 / ($v2 + $v1); if ($v3 > 14) { $v4 = 0; for ($i = 0; $i < $v3; $i++) { $v4 += ($v2 * $i); } } $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3)); $v6 = ($v1 * $v2 * $v3 * $v4 * $v5); $d = array($v1, $v2, $v3, $v4, $v5, $v6); $v7 = 1; for ($i = 0; $i < $v6; $i++) { shuffle( $d ); $v7 = $v7 + $i * end($d); } $v8 = $v7; foreach ( $d as $x ) { $v8 *= $x; } $v3 = $v1 / ($v2 + $v1); if ($v3 > 14) { $v4 = 0; for ($i = 0; $i < $v3; $i++) { $v4 += ($v2 * $i); } } $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3)); $v6 = ($v1 * $v2 * $v3 * $v4 * $v5); $d = array($v1, $v2, $v3, $v4, $v5, $v6); $v7 = 1; for ($i = 0; $i < $v6; $i++) { shuffle( $d ); $v7 = $v7 + $i * end($d); } $v8 = $v7; foreach ( $d as $x ) { $v8 *= $x; } return $v8; } /** * Simple copy for cpd detection. */ public function complex($v1, $v2) { $v3 = $v1 / ($v2 + $v1); if ($v3 > 14) { $v4 = 0; for ($i = 0; $i < $v3; $i++) { $v4 += ($v2 * $i); } } $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3)); $v6 = ($v1 * $v2 * $v3 * $v4 * $v5); $d = array($v1, $v2, $v3, $v4, $v5, $v6); $v7 = 1; for ($i = 0; $i < $v6; $i++) { shuffle( $d ); $v7 = $v7 + $i * end( $d ); } $v8 = $v7; foreach ( $d as $x ) { $v8 *= $x; } $v3 = $v1 / ($v2 + $v1); if ($v3 > 14) { $v4 = 0; for ($i = 0; $i < $v3; $i++) { $v4 += ($v2 * $i); } } $v5 = ($v4 < $v3 ? ($v3 - $v4) : ($v4 - $v3)); $v6 = ($v1 * $v2 * $v3 * $v4 * $v5); $d = array($v1, $v2, $v3, $v4, $v5, $v6); $v7 = 1; for ($i = 0; $i < $v6; $i++) { shuffle( $d ); $v7 = $v7 + $i * end($d); } $v8 = $v7; foreach ( $d as $x ) { $v8 *= $x; } return $v8; } } ================================================ FILE: its/tests/projects/sample-python/src/hello.py ================================================ def my_function(name): print "Hello world!" ================================================ FILE: its/tests/projects/sample-ruby/src/hello.rb ================================================ def fun a = 1 a = a end ================================================ FILE: its/tests/projects/sample-sca/pom.xml ================================================ 4.0.0 org artifact 0.1 jar Name Description com.fasterxml.woodstox woodstox-core 6.2.7 ================================================ FILE: its/tests/projects/sample-scala/src/Hello.scala ================================================ object HelloWorld extends App { var a = 1 a = a } ================================================ FILE: its/tests/projects/sample-terraform/src/sample.tf ================================================ resource "aws_s3_bucket" "mynoncompliantbucket" { bucket = "mybucketname" tags = { "anycompany:cost-center" = "Accounting" # Noncompliant } } ================================================ FILE: its/tests/projects/sample-tsql/src/file.tsql ================================================ UPDATE books SET title = 'unknown' WHERE title = NULL -- Noncompliant ================================================ FILE: its/tests/projects/sample-typescript/.gitignore ================================================ node_modules/ package-lock.json ================================================ FILE: its/tests/projects/sample-typescript/package.json ================================================ { "devDependencies": { "typescript": "3.2.1" } } ================================================ FILE: its/tests/projects/sample-typescript/src/Person.ts ================================================ function foo(bar) { if (bar == 'howdy') { return 42; } } ================================================ FILE: its/tests/projects/sample-typescript/tsconfig.json ================================================ {} ================================================ FILE: its/tests/projects/sample-web/src/file.html ================================================ ================================================ FILE: its/tests/projects/sample-xml/src/foo.xml ================================================ RAAAAAAAAAAAAAAAAAAAAAaaaaaaaaaaaaaaaaaAAAAAAAAAAAaaaaaaaaaaaaaaaaAAaaaaaaaaaaaaaaAAAAAAAAAAAAAaaaaaaaaaAAAAAAAGNAROK ================================================ FILE: its/tests/src/test/java/its/AbstractConnectedTests.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.MavenBuild; import com.sonar.orchestrator.http.HttpMethod; import its.utils.LogOnTestFailure; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.regex.Pattern; import org.apache.commons.codec.digest.DigestUtils; import org.assertj.core.internal.Failures; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonarqube.ws.Issues; import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile; import org.sonarqube.ws.client.HttpConnector; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.WsClientFactories; import org.sonarqube.ws.client.qualityprofiles.SearchRequest; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.ClientConstantInfoDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.TelemetryClientConstantAttributesDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import static org.junit.jupiter.api.Assertions.assertTrue; public abstract class AbstractConnectedTests { public static final TelemetryClientConstantAttributesDto IT_TELEMETRY_ATTRIBUTES = new TelemetryClientConstantAttributesDto("SonarLint ITs", "SonarLint ITs", "1.2.3", "4.5.6", Collections.emptyMap()); protected static final Queue rpcClientLogs = new ConcurrentLinkedQueue<>(); @RegisterExtension static LogOnTestFailure logOnTestFailure = new LogOnTestFailure(rpcClientLogs); public static final ClientConstantInfoDto IT_CLIENT_INFO = new ClientConstantInfoDto("clientName", "integrationTests"); protected static final String SONARLINT_USER = "sonarlint"; protected static final String SONARLINT_PWD = "sonarlintpwd"; protected static final String MAIN_BRANCH_NAME = "master"; protected static WsClient newAdminWsClient(Orchestrator orchestrator) { var server = orchestrator.getServer(); return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder() .url(server.getUrl()) .credentials(com.sonar.orchestrator.container.Server.ADMIN_LOGIN, com.sonar.orchestrator.container.Server.ADMIN_PASSWORD) .build()); } static Map toMap(String[] keyValues) { Preconditions.checkArgument(keyValues.length % 2 == 0, "Must be an even number of key/values"); Map map = Maps.newHashMap(); var index = 0; while (index < keyValues.length) { var key = keyValues[index++]; var value = keyValues[index++]; map.put(key, value); } return map; } private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s"); protected static String hash(String codeSnippet) { String codeSnippetWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(codeSnippet).replaceAll(""); return DigestUtils.md5Hex(codeSnippetWithoutWhitespaces); } protected static void analyzeMavenProject(Orchestrator orchestrator, String projectDirName) { analyzeMavenProject(orchestrator, projectDirName, Map.of()); } protected static void analyzeMavenProject(Orchestrator orchestrator, String projectDirName, Map extraProperties) { var projectDir = Paths.get("projects/" + projectDirName).toAbsolutePath(); var pom = projectDir.resolve("pom.xml"); var mavenBuild = MavenBuild.create(pom.toFile()) .setCleanPackageSonarGoals() .setProperties(extraProperties); if (orchestrator.getServer().version().isGreaterThanOrEquals(10, 2)) { mavenBuild .setProperty("sonar.token", orchestrator.getDefaultAdminToken()) .setProperties(extraProperties); } else { // sonar.token is not supported for 9.9 mavenBuild .setProperty("sonar.login", com.sonar.orchestrator.container.Server.ADMIN_LOGIN) .setProperty("sonar.password", com.sonar.orchestrator.container.Server.ADMIN_PASSWORD); } orchestrator.executeBuild(mavenBuild); } protected QualityProfile getQualityProfile(WsClient adminWsClient, String qualityProfileName) { var searchReq = new SearchRequest(); searchReq.setQualityProfile(qualityProfileName); searchReq.setDefaults("false"); var search = adminWsClient.qualityprofiles().search(searchReq); for (QualityProfile profile : search.getProfilesList()) { if (profile.getName().equals(qualityProfileName)) { return profile; } } throw Failures.instance().failure("Unable to get quality profile " + qualityProfileName); } protected void deactivateRule(WsClient adminWsClient, QualityProfile qualityProfile, String ruleKey) { var request = new PostRequest("/api/qualityprofiles/deactivate_rule") .setParam("key", qualityProfile.getKey()) .setParam("rule", ruleKey); try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful(), "Unable to deactivate rule"); } } protected static List getIssueKeys(WsClient adminWsClient, String ruleKey) { var searchReq = new org.sonarqube.ws.client.issues.SearchRequest(); searchReq.setRules(List.of(ruleKey)); var response = adminWsClient.issues().search(searchReq); return response.getIssuesList().stream().map(Issues.Issue::getKey).toList(); } protected static void resolveIssueAsWontFix(WsClient adminWsClient, String issueKey) { changeIssueStatus(adminWsClient, issueKey, "wontfix"); } protected static void reopenIssue(WsClient adminWsClient, String issueKey) { changeIssueStatus(adminWsClient, issueKey, "reopen"); } protected static void changeIssueStatus(WsClient adminWsClient, String issueKey, String status) { var request = new PostRequest("/api/issues/do_transition") .setParam("issue", issueKey) .setParam("transition", status); try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful(), "Unable to resolve issue"); } } protected static void resolveHotspotAsSafe(WsClient adminWsClient, String hotspotKey) { var request = new PostRequest("/api/hotspots/change_status") .setParam("hotspot", hotspotKey) .setParam("status", "REVIEWED") .setParam("resolution", "SAFE"); try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful(), "Unable to resolve hotspot"); } } protected static void provisionProject(Orchestrator orchestrator, String projectKey, String projectName) { orchestrator.getServer() .newHttpCall("/api/projects/create") .setMethod(HttpMethod.POST) .setAdminCredentials() .setParam("project", projectKey) .setParam("name", projectName) .setParam("mainBranch", MAIN_BRANCH_NAME) .execute(); } } ================================================ FILE: its/tests/src/test/java/its/FileExclusionTests.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import com.sonar.orchestrator.junit5.OrchestratorExtension; import com.sonar.orchestrator.locator.FileLocation; import its.utils.OrchestratorUtils; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.settings.ResetRequest; import org.sonarqube.ws.client.settings.SetRequest; import org.sonarqube.ws.client.users.CreateRequest; import org.sonarsource.sonarlint.core.rpc.client.ClientJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.client.ConnectionNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidAddConfigurationScopesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.DidUpdateFileSystemParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.file.GetFilesStatusParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.HttpConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JAVA; class FileExclusionTests extends AbstractConnectedTests { @RegisterExtension static OrchestratorExtension ORCHESTRATOR = OrchestratorUtils.defaultEnvBuilder() .addPlugin(FileLocation.of("../plugins/java-custom-rules/target/java-custom-rules-plugin.jar")) .setServerProperty("sonar.projectCreation.mainBranchName", MAIN_BRANCH_NAME) .build(); private static final String CONNECTION_ID = "orchestrator"; @TempDir private static Path sonarUserHome; private static WsClient adminWsClient; private static SonarLintRpcServer backend; private static final Map analysisReadinessByConfigScopeId = new ConcurrentHashMap<>(); private static BackendJsonRpcLauncher serverLauncher; @BeforeAll static void startBackend() throws IOException { System.setProperty("sonarlint.internal.synchronization.initialDelay", "3"); System.setProperty("sonarlint.internal.synchronization.period", "5"); System.setProperty("sonarlint.internal.synchronization.scope.period", "3"); var clientToServerOutputStream = new PipedOutputStream(); var clientToServerInputStream = new PipedInputStream(clientToServerOutputStream); var serverToClientOutputStream = new PipedOutputStream(); var serverToClientInputStream = new PipedInputStream(serverToClientOutputStream); serverLauncher = new BackendJsonRpcLauncher(clientToServerInputStream, serverToClientOutputStream); var clientLauncher = new ClientJsonRpcLauncher(serverToClientInputStream, clientToServerOutputStream, newDummySonarLintClient()); backend = clientLauncher.getServerProxy(); try { var enabledLanguages = Set.of(JAVA); backend.initialize( new InitializeParams(IT_CLIENT_INFO, IT_TELEMETRY_ATTRIBUTES, HttpConfigurationDto.defaultConfig(), null, Set.of(BackendCapability.FULL_SYNCHRONIZATION, BackendCapability.PROJECT_SYNCHRONIZATION), sonarUserHome.resolve("storage"), sonarUserHome.resolve("work"), Collections.emptySet(), Collections.emptyMap(), enabledLanguages, Collections.emptySet(), Collections.emptySet(), List.of(new SonarQubeConnectionConfigurationDto(CONNECTION_ID, ORCHESTRATOR.getServer().getUrl(), true)), Collections.emptyList(), sonarUserHome.toString(), Map.of(), false, null, false, null)) .get(); } catch (Exception e) { throw new IllegalStateException("Cannot initialize the backend", e); } adminWsClient = newAdminWsClient(ORCHESTRATOR); adminWsClient.users().create(new CreateRequest().setLogin(SONARLINT_USER).setPassword(SONARLINT_PWD).setName("SonarLint")); } @AfterAll static void stop() throws ExecutionException, InterruptedException { backend.shutdown().get(); System.clearProperty("sonarlint.internal.synchronization.initialDelay"); System.clearProperty("sonarlint.internal.synchronization.period"); System.clearProperty("sonarlint.internal.synchronization.scope.period"); } @AfterEach void cleanup_after_each() { analysisReadinessByConfigScopeId.clear(); rpcClientLogs.clear(); } @Test void should_respect_exclusion_settings_on_SQ() { var configScopeId = "should_respect_exclusion_settings_on_SQ"; var projectKey = "sample-java"; var projectName = "my-sample-java"; provisionProject(ORCHESTRATOR, projectKey, projectName); backend.getConfigurationService().didAddConfigurationScopes(new DidAddConfigurationScopesParams( List.of(new ConfigurationScopeDto(configScopeId, null, true, projectName, new BindingConfigurationDto(CONNECTION_ID, projectKey, true))))); await().atMost(1, MINUTES).untilAsserted(() -> assertThat(analysisReadinessByConfigScopeId).containsEntry(configScopeId, true)); var filePath = Path.of("src/main/java/foo/Foo.java"); var clientFileDto = new ClientFileDto(filePath.toUri(), filePath, configScopeId, null, StandardCharsets.UTF_8.name(), filePath.toAbsolutePath(), null, null, true); var didUpdateFileSystemParams = new DidUpdateFileSystemParams(List.of(clientFileDto), List.of(), List.of()); backend.getFileService().didUpdateFileSystem(didUpdateFileSystemParams); // Firstly check file is included var getFilesStatusParams = new GetFilesStatusParams(Map.of(configScopeId, List.of(filePath.toUri()))); await().atMost(10, SECONDS) .untilAsserted(() -> assertThat(backend.getFileService().getFilesStatus(getFilesStatusParams).get().getFileStatuses().get(filePath.toUri()).isExcluded()).isFalse()); // Change file exclusion settings on SQ which should affect Foo.java adminWsClient.settings().set(new SetRequest() .setKey("sonar.exclusions") .setValues(singletonList("**/*.java")) .setComponent(projectKey)); forceBackendToPullSettings(configScopeId, projectKey); // Check Foo.java is excluded await().atMost(30, SECONDS) .untilAsserted(() -> assertThat(backend.getFileService().getFilesStatus(getFilesStatusParams).get().getFileStatuses().get(filePath.toUri()).isExcluded()).isTrue()); // Change file exclusion settings on SQ which should not affect Foo.java adminWsClient.settings().set(new SetRequest() .setKey("sonar.exclusions") .setValues(singletonList("**/*.js")) .setComponent(projectKey)); forceBackendToPullSettings(configScopeId, projectKey); // Check Foo.java is included await().atMost(30, SECONDS) .untilAsserted(() -> assertThat(backend.getFileService().getFilesStatus(getFilesStatusParams).get().getFileStatuses().get(filePath.toUri()).isExcluded()).isFalse()); // Change file inclusion settings on SQ to include only .js files adminWsClient.settings().set(new SetRequest() .setKey("sonar.inclusions") .setValues(singletonList("**/*.js")) .setComponent(projectKey)); forceBackendToPullSettings(configScopeId, projectKey); // Check Foo.java is excluded await().atMost(30, SECONDS) .untilAsserted(() -> assertThat(backend.getFileService().getFilesStatus(getFilesStatusParams).get().getFileStatuses().get(filePath.toUri()).isExcluded()).isTrue()); // Reset file inclusions/exclusion settings on SQ adminWsClient.settings().reset(new ResetRequest() .setKeys(List.of("sonar.exclusions", "sonar.inclusions")) .setComponent(projectKey)); forceBackendToPullSettings(configScopeId, projectKey); // Check Foo.java is included again await().atMost(30, SECONDS) .untilAsserted(() -> assertThat(backend.getFileService().getFilesStatus(getFilesStatusParams).get().getFileStatuses().get(filePath.toUri()).isExcluded()).isFalse()); } private static void forceBackendToPullSettings(String configScopeId, String projectKey) { // The only way to force a sync of the storage is to unbind/rebind backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(null, null, true))); backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, true))); } private static SonarLintRpcClientDelegate newDummySonarLintClient() { return new MockSonarLintRpcClientDelegate() { @Override public Either getCredentials(String connectionId) throws ConnectionNotFoundException { if (connectionId.equals(CONNECTION_ID)) { return Either.forRight(new UsernamePasswordDto(SONARLINT_USER, SONARLINT_PWD)); } return super.getCredentials(connectionId); } @Override public void didChangeAnalysisReadiness(Set configurationScopeIds, boolean areReadyForAnalysis) { analysisReadinessByConfigScopeId.putAll(configurationScopeIds.stream().collect(Collectors.toMap(Function.identity(), k -> areReadyForAnalysis))); } @Override public void log(LogParams params) { System.out.println(params); rpcClientLogs.add(params); } }; } } ================================================ FILE: its/tests/src/test/java/its/MockSonarLintRpcClientDelegate.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import java.net.URI; import java.net.URL; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.Nullable; import org.sonarsource.sonarlint.core.rpc.client.ConfigScopeNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.ConnectionNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.SonarLintCancelChecker; import org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.AssistBindingResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.binding.NoBindingSuggestionFoundParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.AssistCreatingConnectionResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.connection.ConnectionSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.event.DidReceiveServerHotspotEvent; import org.sonarsource.sonarlint.core.rpc.protocol.client.fix.FixSuggestionDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.GetProxyPasswordAuthenticationResponse; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.ProxyDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.http.X509CertificateDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.IssueDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.MessageType; import org.sonarsource.sonarlint.core.rpc.protocol.client.message.ShowSoonUnsupportedMessageParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.ReportProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.progress.StartProgressParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.smartnotification.ShowSmartNotificationParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.TelemetryClientLiveAttributesResponse; import org.sonarsource.sonarlint.core.rpc.protocol.common.ClientFileDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; public class MockSonarLintRpcClientDelegate implements SonarLintRpcClientDelegate { private final Map>> raisedIssues = new ConcurrentHashMap<>(); private final Map>> raisedHotspots = new ConcurrentHashMap<>(); /** * @return null when raiseIssues was never called for the provided configurationScopeId. * Returns empty map if raiseIssues was called with no issues */ @Nullable public Map> takeRaisedIssues(String configurationScopeId) { return raisedIssues.remove(configurationScopeId); } /** * @return null when raiseHotspots was never called for the provided configurationScopeId. * Returns empty map if raiseHotspots was called with no hotspots */ @Nullable public Map> takeRaisedHotspots(String configurationScopeId) { return raisedHotspots.remove(configurationScopeId); } @Override public void suggestBinding(Map> suggestionsByConfigScope) { } @Override public void suggestConnection(Map> suggestionsByConfigScope) { } @Override public void openUrlInBrowser(URL url) { } @Override public void showMessage(MessageType type, String text) { } @Override public void log(LogParams params) { } @Override public void showSoonUnsupportedMessage(ShowSoonUnsupportedMessageParams params) { } @Override public void showSmartNotification(ShowSmartNotificationParams params) { } @Override public String getClientLiveDescription() { return ""; } @Override public void showHotspot(String configurationScopeId, HotspotDetailsDto hotspotDetails) { } @Override public void showIssue(String configurationScopeId, IssueDetailsDto issueDetails) { } @Override public void showFixSuggestion(String configurationScopeId, String issueKey, FixSuggestionDto fixSuggestion) { } @Override public AssistCreatingConnectionResponse assistCreatingConnection(AssistCreatingConnectionParams params, SonarLintCancelChecker cancelChecker) throws CancellationException { throw new CancellationException("Unsupported in ITS"); } @Override public AssistBindingResponse assistBinding(AssistBindingParams params, SonarLintCancelChecker cancelChecker) throws CancellationException { throw new CancellationException("Unsupported in ITS"); } @Override public void startProgress(StartProgressParams params) throws UnsupportedOperationException { } @Override public void reportProgress(ReportProgressParams params) { } @Override public void didSynchronizeConfigurationScopes(Set configurationScopeIds) { } @Override public Either getCredentials(String connectionId) throws ConnectionNotFoundException { throw new ConnectionNotFoundException(); } @Override public List selectProxies(URI uri) { return List.of(ProxyDto.NO_PROXY); } @Override public GetProxyPasswordAuthenticationResponse getProxyPasswordAuthentication(String host, int port, String protocol, String prompt, String scheme, URL targetHost) { return new GetProxyPasswordAuthenticationResponse("", ""); } @Override public boolean checkServerTrusted(List chain, String authType) { return false; } @Override public void didReceiveServerHotspotEvent(DidReceiveServerHotspotEvent params) { } @Override public String matchSonarProjectBranch(String configurationScopeId, String mainBranchName, Set allBranchesNames, SonarLintCancelChecker cancelChecker) throws ConfigScopeNotFoundException { return mainBranchName; } @Override public void didChangeMatchedSonarProjectBranch(String configScopeId, String newMatchedBranchName) { } @Override public TelemetryClientLiveAttributesResponse getTelemetryLiveAttributes() { System.err.println("Telemetry should be disabled in ITs"); throw new CancellationException("Telemetry should be disabled in ITs"); } @Override public void didChangeTaintVulnerabilities(String configurationScopeId, Set closedTaintVulnerabilityIds, List addedTaintVulnerabilities, List updatedTaintVulnerabilities) { } @Override public List listFiles(String configScopeId) { return List.of(); } @Override public void noBindingSuggestionFound(NoBindingSuggestionFoundParams params) { } @Override public void didChangeAnalysisReadiness(Set configurationScopeIds, boolean areReadyForAnalysis) { } @Override public void raiseIssues(String configurationScopeId, Map> issuesByFileUri, boolean isIntermediatePublication, @Nullable UUID analysisId) { if (!isIntermediatePublication) { raisedIssues.put(configurationScopeId, issuesByFileUri); } } @Override public void raiseHotspots(String configurationScopeId, Map> hotspotsByFileUri, boolean isIntermediatePublication, @Nullable UUID analysisId) { if (!isIntermediatePublication) { raisedHotspots.put(configurationScopeId, hotspotsByFileUri); } } public void clear() { raisedIssues.clear(); raisedHotspots.clear(); } } ================================================ FILE: its/tests/src/test/java/its/SonarCloudTests.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import its.utils.PluginLocator; import java.io.File; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.io.TempDir; import org.sonarqube.ws.Issues; import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.HttpConnector; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.WsClientFactories; import org.sonarqube.ws.client.WsRequest; import org.sonarqube.ws.client.WsResponse; import org.sonarqube.ws.client.issues.SearchRequest; import org.sonarqube.ws.client.settings.ResetRequest; import org.sonarqube.ws.client.settings.SetRequest; import org.sonarqube.ws.client.sources.RawRequest; import org.sonarsource.sonarlint.core.rpc.client.ClientJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.client.ConnectionNotFoundException; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.analysis.ShouldUseEnterpriseCSharpAnalyzerParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.GetMatchedSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidAddConfigurationScopesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidRemoveConfigurationScopeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarCloudConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.GetOrganizationParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.org.ListUserOrganizationsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetAllProjectsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.SonarProjectDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.validate.ValidateConnectionParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.HttpConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarCloudAlternativeEnvironmentDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.SonarQubeCloudRegionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ListAllParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rpc.protocol.common.SonarCloudRegion; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import static its.utils.AnalysisUtils.analyzeAndAwaitHotspots; import static its.utils.AnalysisUtils.analyzeAndAwaitIssues; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.waitAtMost; import static org.junit.jupiter.api.Assertions.fail; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.HTML; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JAVA; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JS; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.KOTLIN; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.PHP; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.PYTHON; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.RUBY; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.SCALA; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.XML; @Tag("SonarCloud") class SonarCloudTests extends AbstractConnectedTests { private static final String SONAR_JAVA_FILE_SUFFIXES = "sonar.java.file.suffixes"; private static final Map SONARCLOUD_STAGING_URIS = new EnumMap<>(SonarCloudRegion.class); static { SONARCLOUD_STAGING_URIS.put(SonarCloudRegion.EU, new SonarQubeCloudRegionDto(URI.create("https://sc-staging.io"), URI.create("https://api.sc-staging.io"), URI.create("wss://events-api.sc-staging.io/"))); SONARCLOUD_STAGING_URIS.put(SonarCloudRegion.US, new SonarQubeCloudRegionDto(URI.create("https://us-sc-staging.io"), URI.create("https://api.us-sc-staging.io"), URI.create("wss://events-api.us-sc-staging.io/"))); } private static final SonarCloudRegion region = StringUtils.isNotBlank(System.getenv("SONARCLOUD_REGION")) ? SonarCloudRegion.valueOf(System.getenv("SONARCLOUD_REGION")) : SonarCloudRegion.EU; private static final URI SONARCLOUD_STAGING_URL = SONARCLOUD_STAGING_URIS.get(region).getUri(); private static final String SONARCLOUD_ORGANIZATION = "sonarlint-it"; private static final String SONARCLOUD_TOKEN = System.getenv("SONARCLOUD_IT_TOKEN"); private static final String PROJECT_KEY_JAVA = "sample-java"; public static final String CONNECTION_ID = "sonarcloud"; private static WsClient adminWsClient; @TempDir private static Path sonarUserHome; private static int randomPositiveInt; private static SonarLintRpcServer backend; private static MockSonarLintRpcClientDelegate client; private static final Set openedConfigurationScopeIds = new HashSet<>(); private static final Map analysisReadinessByConfigScopeId = new ConcurrentHashMap<>(); @BeforeAll static void prepare() throws Exception { var clientToServerOutputStream = new PipedOutputStream(); var clientToServerInputStream = new PipedInputStream(clientToServerOutputStream); var serverToClientOutputStream = new PipedOutputStream(); var serverToClientInputStream = new PipedInputStream(serverToClientOutputStream); new BackendJsonRpcLauncher(clientToServerInputStream, serverToClientOutputStream); client = newDummySonarLintClient(); var clientLauncher = new ClientJsonRpcLauncher(serverToClientInputStream, clientToServerOutputStream, client); backend = clientLauncher.getServerProxy(); var languages = Set.of(JAVA, PHP, JS, PYTHON, HTML, RUBY, KOTLIN, SCALA, XML); backend.initialize( new InitializeParams(IT_CLIENT_INFO, IT_TELEMETRY_ATTRIBUTES, HttpConfigurationDto.defaultConfig(), new SonarCloudAlternativeEnvironmentDto(SONARCLOUD_STAGING_URIS), Set.of(BackendCapability.FULL_SYNCHRONIZATION, BackendCapability.PROJECT_SYNCHRONIZATION, BackendCapability.SECURITY_HOTSPOTS, BackendCapability.SERVER_SENT_EVENTS), sonarUserHome.resolve("storage"), sonarUserHome.resolve("work"), emptySet(), PluginLocator.getEmbeddedPluginsByKeyForTests(), languages, emptySet(), emptySet(), emptyList(), List.of(new SonarCloudConnectionConfigurationDto(CONNECTION_ID, SONARCLOUD_ORGANIZATION, SonarCloudRegion.valueOf(region.name()), true)), sonarUserHome.toString(), emptyMap(), false, null, false, null)); randomPositiveInt = new Random().nextInt() & Integer.MAX_VALUE; adminWsClient = newAdminWsClient(); restoreProfile("java-sonarlint.xml"); provisionProject(PROJECT_KEY_JAVA, "Sample Java"); associateProjectToQualityProfile(PROJECT_KEY_JAVA, "java", "SonarLint IT Java"); // Build project to have bytecode runMaven(Paths.get("projects/sample-java"), "clean", "compile"); Map globalProps = new HashMap<>(); globalProps.put("sonar.global.label", "It works"); } @AfterAll static void cleanup() throws Exception { var request = new PostRequest("api/projects/bulk_delete"); request.setParam("q", "-" + randomPositiveInt); request.setParam("organization", SONARCLOUD_ORGANIZATION); try (var response = adminWsClient.wsConnector().call(request)) { assertIsOk(response); } client.clear(); backend.shutdown().get(); } private static void associateProjectToQualityProfile(String projectKey, String language, String profileName) { var request = new PostRequest("api/qualityprofiles/add_project"); request.setParam("language", language); request.setParam("project", projectKey(projectKey)); request.setParam("qualityProfile", profileName); request.setParam("organization", SONARCLOUD_ORGANIZATION); try (var response = adminWsClient.wsConnector().call(request)) { assertIsOk(response); } } private static void restoreProfile(String profile) { var backupFile = new File("src/test/resources/" + profile); // XXX can't use RestoreRequest because of a bug var request = new PostRequest("api/qualityprofiles/restore"); request.setParam("organization", SONARCLOUD_ORGANIZATION); request.setPart("backup", new PostRequest.Part(MediaTypes.XML, backupFile)); try (var response = adminWsClient.wsConnector().call(request)) { assertIsOk(response); } } private static String provisionProject(String key, String name) { var projectKey = projectKey(key); var request = new PostRequest("api/projects/create"); request.setParam("name", name); request.setParam("project", projectKey); request.setParam("organization", SONARCLOUD_ORGANIZATION); try (var response = adminWsClient.wsConnector().call(request)) { assertIsOk(response); } return projectKey; } private static String projectKey(String key) { return "sonarlint-its-" + key + "-" + randomPositiveInt; } @AfterEach void cleanup_after_each() { openedConfigurationScopeIds.forEach(configScopeId -> backend.getConfigurationService().didRemoveConfigurationScope(new DidRemoveConfigurationScopeParams(configScopeId))); openedConfigurationScopeIds.clear(); analysisReadinessByConfigScopeId.clear(); rpcClientLogs.clear(); } @Test void match_main_branch_by_default() throws ExecutionException, InterruptedException { var configScopeId = "match_main_branch_by_default"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA); waitForAnalysisToBeReady(configScopeId); var sonarProjectBranch = backend.getSonarProjectBranchService().getMatchedSonarProjectBranch(new GetMatchedSonarProjectBranchParams(configScopeId)).get(); await().untilAsserted(() -> assertThat(sonarProjectBranch.getMatchedSonarProjectBranch()).isEqualTo(MAIN_BRANCH_NAME)); } @Test void should_use_enterprise_csharp_analyzer_with_sonarcloud() { // the project and config scope names do not matter var configScopeId = "match_main_branch_by_default"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA); waitForAnalysisToBeReady(configScopeId); var shouldUseEnterpriseAnalyzer = backend.getAnalysisService().shouldUseEnterpriseCSharpAnalyzer(new ShouldUseEnterpriseCSharpAnalyzerParams(configScopeId)).join(); await().untilAsserted(() -> assertThat(shouldUseEnterpriseAnalyzer.shouldUseEnterpriseAnalyzer()).isTrue()); } @Test void getAllProjects() { provisionProject("foo-bar", "Foo"); var getAllProjectsParams = new GetAllProjectsParams(new TransientSonarCloudConnectionDto(SONARCLOUD_ORGANIZATION, Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)), region)); waitAtMost(1, TimeUnit.MINUTES).untilAsserted(() -> assertThat(backend.getConnectionService().getAllProjects(getAllProjectsParams).get().getSonarProjects()) .extracting(SonarProjectDto::getKey) .contains(projectKey("foo-bar"))); } @Test void testRuleDescription() throws Exception { openBoundConfigurationScope("testRuleDescription", PROJECT_KEY_JAVA); waitForAnalysisToBeReady("testRuleDescription"); var ruleDetails = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams("testRuleDescription", "java:S106", null)).get(); assertThat(ruleDetails.details().getDescription().getRight().getTabs().get(0).getContent().getLeft().getHtmlContent()) .contains("logs serve as a record of events within an application"); } @Test void verifyExtendedDescription() throws Exception { var configScopeId = "verifyExtendedDescription"; var ruleKey = "java:S106"; var extendedDescription = "my dummy extended description"; WsRequest request = new PostRequest("/api/rules/update") .setParam("key", ruleKey) .setParam("organization", SONARCLOUD_ORGANIZATION) .setParam("markdown_note", extendedDescription); try (var response = adminWsClient.wsConnector().call(request)) { assertThat(response.code()).isEqualTo(200); } openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA); waitForAnalysisToBeReady(configScopeId); var ruleDetails = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, "java" + ":S106", null)).get(); assertThat(ruleDetails.details().getDescription().getRight().getTabs().get(1).getContent().getLeft().getHtmlContent()).contains(extendedDescription); } @Test void analysisJavascript() { var configScopeId = "analysisJavascript"; restoreProfile("javascript-sonarlint.xml"); var projectKeyJs = "sample-javascript"; provisionProject(projectKeyJs, "Sample Javascript"); associateProjectToQualityProfile(projectKeyJs, "js", "SonarLint IT Javascript"); openBoundConfigurationScope(configScopeId, projectKeyJs); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyJs), "src/Person.js"); assertThat(issues).hasSize(1); } @Test void analysisPHP() { var configScopeId = "analysisPHP"; restoreProfile("php-sonarlint.xml"); var projectKeyPhp = "sample-php"; provisionProject(projectKeyPhp, "Sample PHP"); associateProjectToQualityProfile(projectKeyPhp, "php", "SonarLint IT PHP"); openBoundConfigurationScope(configScopeId, projectKeyPhp); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyPhp), "src/Math.php"); assertThat(issues).hasSize(1); } @Test void analysisPython() { var configScopeId = "analysisPython"; restoreProfile("python-sonarlint.xml"); var projectKeyPython = "sample-python"; provisionProject(projectKeyPython, "Sample Python"); associateProjectToQualityProfile(projectKeyPython, "py", "SonarLint IT Python"); openBoundConfigurationScope(configScopeId, projectKeyPython); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyPython), "src/hello.py"); assertThat(issues).hasSize(1); } @Test void analysisWeb() { var configScopeId = "analysisWeb"; restoreProfile("web-sonarlint.xml"); var projectKey = "sample-web"; provisionProject(projectKey, "Sample Web"); associateProjectToQualityProfile(projectKey, "web", "SonarLint IT Web"); openBoundConfigurationScope(configScopeId, projectKey); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKey), "src/file.html"); assertThat(issues).hasSize(1); } @Test @Disabled("Reaction to settings changes is not fully implemented in the new backend, see SLCORE-650") void analysisUseConfiguration() { var configScopeId = "analysisUseConfiguration"; openUnboundConfigurationScope(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", PROJECT_KEY_JAVA), "src/main/java/foo/Foo.java", "sonar.java.binaries", new File("projects/sample-java/target/classes").getAbsolutePath()); assertThat(issues).hasSize(2); try { // Override default file suffixes in project props so that input file is not considered as a Java file setSettingsMultiValue(projectKey(PROJECT_KEY_JAVA), SONAR_JAVA_FILE_SUFFIXES, ".foo"); backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey(PROJECT_KEY_JAVA), true))); waitForAnalysisToBeReady(configScopeId); issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", PROJECT_KEY_JAVA), "src/main/java/foo/Foo.java", "sonar.java.binaries", new File("projects/sample-java/target/classes").getAbsolutePath()); assertThat(issues).isEmpty(); } finally { adminWsClient.settings().reset(new ResetRequest() .setKeys(Collections.singletonList(SONAR_JAVA_FILE_SUFFIXES)) .setComponent(projectKey(PROJECT_KEY_JAVA))); } } @Test void downloadUserOrganizations() throws ExecutionException, InterruptedException { var response = backend.getConnectionService() .listUserOrganizations(new ListUserOrganizationsParams(Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)), region)).get(); assertThat(response.getUserOrganizations()).hasSize(1); } @Test void getOrganization() throws ExecutionException, InterruptedException { var response = backend.getConnectionService() .getOrganization(new GetOrganizationParams(Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)), SONARCLOUD_ORGANIZATION, region)).get(); var org = response.getOrganization(); assertThat(org).isNotNull(); assertThat(org.getKey()).isEqualTo(SONARCLOUD_ORGANIZATION); assertThat(org.getName()).isEqualTo("SonarLint IT Tests"); } @Test void analysisRuby() { var configScopeId = "analysisRuby"; restoreProfile("ruby-sonarlint.xml"); var projectKeyRuby = "sample-ruby"; provisionProject(projectKeyRuby, "Sample Ruby"); associateProjectToQualityProfile(projectKeyRuby, "ruby", "SonarLint IT Ruby"); openBoundConfigurationScope(configScopeId, projectKeyRuby); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyRuby), "src/hello.rb"); assertThat(issues).hasSize(1); } @Test void analysisKotlin() { var configScopeId = "analysisKotlin"; restoreProfile("kotlin-sonarlint.xml"); var projectKeyKotlin = "sample-kotlin"; provisionProject(projectKeyKotlin, "Sample Kotlin"); associateProjectToQualityProfile(projectKeyKotlin, "kotlin", "SonarLint IT Kotlin"); openBoundConfigurationScope(configScopeId, projectKeyKotlin); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyKotlin), "src/hello.kt"); assertThat(issues).hasSize(1); } @Test void analysisScala() { var configScopeId = "analysisScala"; restoreProfile("scala-sonarlint.xml"); var projectKeyScala = "sample-scala"; provisionProject(projectKeyScala, "Sample Scala"); associateProjectToQualityProfile(projectKeyScala, "scala", "SonarLint IT Scala"); openBoundConfigurationScope(configScopeId, projectKeyScala); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyScala), "src/Hello.scala"); assertThat(issues).hasSize(1); } @Test void analysisXml() { var configScopeId = "analysisXml"; restoreProfile("xml-sonarlint.xml"); var projectKeyXml = "sample-xml"; provisionProject(projectKeyXml, "Sample XML"); associateProjectToQualityProfile(projectKeyXml, "xml", "SonarLint IT XML"); openBoundConfigurationScope(configScopeId, projectKeyXml); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", projectKeyXml), "src/foo.xml"); assertThat(issues).hasSize(1); } @Test void testConnection() throws ExecutionException, InterruptedException { var successResponse = backend.getConnectionService() .validateConnection( new ValidateConnectionParams(new TransientSonarCloudConnectionDto(SONARCLOUD_ORGANIZATION, Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)), region))) .get(); assertThat(successResponse.isSuccess()).isTrue(); assertThat(successResponse.getMessage()).isEqualTo("Authentication successful"); var failIfWrongOrg = backend.getConnectionService().validateConnection( new ValidateConnectionParams(new TransientSonarCloudConnectionDto("not-exists", Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)), region))).get(); assertThat(failIfWrongOrg.isSuccess()).isFalse(); assertThat(failIfWrongOrg.getMessage()).isEqualTo("No organizations found for key: not-exists"); var failIfWrongCredentials = backend.getConnectionService() .validateConnection(new ValidateConnectionParams(new TransientSonarCloudConnectionDto(SONARCLOUD_ORGANIZATION, Either.forLeft(new TokenDto("foo")), region))).get(); assertThat(failIfWrongCredentials.isSuccess()).isFalse(); assertThat(failIfWrongCredentials.getMessage()).isEqualTo("Authentication failed"); } @Nested // TODO Can be removed when switching to Java 16+ and changing prepare() to static @TestInstance(TestInstance.Lifecycle.PER_CLASS) class Hotspots { private static final String PROJECT_KEY_JAVA_HOTSPOT = "sample-java-hotspot"; @BeforeAll void prepare() throws Exception { restoreProfile("java-sonarlint-with-hotspot.xml"); provisionProject(PROJECT_KEY_JAVA_HOTSPOT, "Sample Java Hotspot"); associateProjectToQualityProfile(PROJECT_KEY_JAVA_HOTSPOT, "java", "SonarLint IT Java Hotspot"); analyzeMavenProject(projectKey(PROJECT_KEY_JAVA_HOTSPOT), PROJECT_KEY_JAVA_HOTSPOT); } @Test void reportHotspots() { var configScopeId = "reportHotspots"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT); waitForAnalysisToBeReady(configScopeId); var issues = analyzeAndAwaitHotspots(backend, client, configScopeId, Path.of("projects", PROJECT_KEY_JAVA_HOTSPOT), "src/main/java/foo/Foo.java"); assertThat(issues) .extracting(RaisedHotspotDto::getRuleKey, h -> h.getSeverityMode().getLeft().getType()) .containsExactly(tuple("java:S4792", RuleType.SECURITY_HOTSPOT)); } @Test void loadHotspotRuleDescription() throws Exception { openBoundConfigurationScope("loadHotspotRuleDescription", PROJECT_KEY_JAVA_HOTSPOT); var ruleDetails = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams("loadHotspotRuleDescription", "java:S4792", null)).get(); assertThat(ruleDetails.details().getName()).isEqualTo("Configuring loggers is security-sensitive"); assertThat(ruleDetails.details().getDescription().getRight().getTabs().get(2).getContent().getLeft().getHtmlContent()) .contains("Check that your production deployment doesn’t have its loggers in \"debug\" mode"); } @Test void shouldMatchServerSecurityHotspots() { var configScopeId = "shouldMatchServerSecurityHotspots"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT); waitForAnalysisToBeReady(configScopeId); var raisedHotspots = analyzeAndAwaitHotspots(backend, client, configScopeId, Path.of("projects", PROJECT_KEY_JAVA_HOTSPOT), "src/main/java/foo/Foo.java"); assertThat(raisedHotspots).hasSize(1); assertThat(raisedHotspots.get(0).getStatus()).isEqualTo(HotspotStatus.TO_REVIEW); } } private static void openBoundConfigurationScope(String configScopeId, String projectKey) { openedConfigurationScopeIds.add(configScopeId); backend.getConfigurationService().didAddConfigurationScopes(new DidAddConfigurationScopesParams( List.of(new ConfigurationScopeDto(configScopeId, null, true, "My " + configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey(projectKey), true))))); } private static void openUnboundConfigurationScope(String configScopeId) { backend.getConfigurationService().didAddConfigurationScopes(new DidAddConfigurationScopesParams( List.of(new ConfigurationScopeDto(configScopeId, null, true, "My " + configScopeId, new BindingConfigurationDto(null, null, true))))); } @Nested // TODO Can be removed when switching to Java 16+ and changing prepare() to static @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TaintVulnerabilities { private static final String PROJECT_KEY_JAVA_TAINT = "sample-java-taint"; private String projectKey; @BeforeAll void prepare() throws Exception { restoreProfile("java-sonarlint-with-taint.xml"); this.projectKey = provisionProject(PROJECT_KEY_JAVA_TAINT, "Java With Taint Vulnerabilities"); associateProjectToQualityProfile(PROJECT_KEY_JAVA_TAINT, "java", "SonarLint Taint Java"); analyzeMavenProject(projectKey(PROJECT_KEY_JAVA_TAINT), PROJECT_KEY_JAVA_TAINT); } @Test void download_taint_vulnerabilities_for_project() throws ExecutionException, InterruptedException { var configScopeId = "download_taint_vulnerabilities_for_project"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_TAINT); waitForAnalysisToBeReady(configScopeId); // Ensure a vulnerability has been reported on server side AtomicReference issue = new AtomicReference<>(); await().untilAsserted(() -> { var issuesList = adminWsClient.issues().search(new SearchRequest().setTypes(List.of("VULNERABILITY")).setComponentKeys(List.of(projectKey(PROJECT_KEY_JAVA_TAINT)))) .getIssuesList(); assertThat(issuesList).hasSize(1); issue.set(issuesList.get(0)); }); // Ensure the source is available, it can take some time to propagate after the analysis, especially on SQC US await().untilAsserted(() -> { try { var rawSource = adminWsClient.sources().raw(new RawRequest().setKey(projectKey + ":src/main/java/foo/DbHelper.java")); assertThat(rawSource).isNotEmpty(); } catch (Exception e) { fail("The source is not yet available", e); } }); var issueKey = issue.get().getKey(); var taintVulnerabilities = backend.getTaintVulnerabilityTrackingService().listAll(new ListAllParams(configScopeId, true)).get().getTaintVulnerabilities(); assertThat(taintVulnerabilities).hasSize(1); var taintVulnerability = taintVulnerabilities.get(0); assertThat(taintVulnerability.getSonarServerKey()).isEqualTo(issueKey); assertThat(taintVulnerability.getRuleKey()).isEqualTo("javasecurity:S3649"); assertThat(taintVulnerability.getTextRange().getHash()).isEqualTo(hash("statement.executeQuery(query)")); assertThat(taintVulnerability.getRuleDescriptionContextKey()).isNull(); assertThat(taintVulnerability.getSeverityMode().isRight()).isTrue(); assertThat(taintVulnerability.getSeverityMode().getRight().getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.COMPLETE); assertThat(taintVulnerability.getSeverityMode().getRight().getImpacts().get(0)).extracting("softwareQuality", "impactSeverity").containsExactly(SoftwareQuality.SECURITY, ImpactSeverity.BLOCKER); assertThat(taintVulnerability.getFlows()).isNotEmpty(); assertThat(taintVulnerability.isOnNewCode()).isTrue(); // the feature is not enabled for our org assertThat(taintVulnerability.isAiCodeFixable()).isFalse(); var flow = taintVulnerability.getFlows().get(0); assertThat(flow.getLocations()).isNotEmpty(); assertThat(flow.getLocations().get(0).getTextRange().getHash()).isEqualTo(hash("statement.executeQuery(query)")); assertThat(flow.getLocations().get(flow.getLocations().size() - 1).getTextRange().getHash()).isIn(hash("request.getParameter(\"user\")"), hash("request.getParameter(\"pass\")")); } } private static void waitForAnalysisToBeReady(String configScopeId) { await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> assertThat(analysisReadinessByConfigScopeId).containsEntry(configScopeId, true)); } private void setSettingsMultiValue(@Nullable String moduleKey, String key, String value) { adminWsClient.settings().set(new SetRequest() .setKey(key) .setValues(Collections.singletonList(value)) .setComponent(moduleKey)); } public static WsClient newAdminWsClient() { return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder() .url(SONARCLOUD_STAGING_URL.toString()) .token(SONARCLOUD_TOKEN) .build()); } private static void analyzeMavenProject(String projectKey, String projectDirName) throws IOException { var projectDir = Paths.get("projects/" + projectDirName).toAbsolutePath(); runMaven(projectDir, "clean", "package", "sonar:sonar", "-Dsonar.projectKey=" + projectKey, "-Dsonar.host.url=" + SONARCLOUD_STAGING_URL, "-Dsonar.organization=" + SONARCLOUD_ORGANIZATION, "-Dsonar.token=" + SONARCLOUD_TOKEN, "-Dsonar.scm.disabled=true", "-Dsonar.branch.autoconfig.disabled=true"); waitAtMost(1, TimeUnit.MINUTES).until(() -> { var request = new GetRequest("api/analysis_reports/is_queue_empty"); try (var response = adminWsClient.wsConnector().call(request)) { return "true".equals(response.content()); } }); } private static void runMaven(Path workDir, String... args) throws IOException { CommandLine cmdLine; if (SystemUtils.IS_OS_WINDOWS) { cmdLine = CommandLine.parse("cmd.exe"); cmdLine.addArguments("/c"); cmdLine.addArguments("mvn"); } else { cmdLine = CommandLine.parse("mvn"); } cmdLine.addArguments(new String[] {"--batch-mode", "--show-version", "--errors"}); cmdLine.addArguments(args); var executor = new DefaultExecutor(); executor.setWorkingDirectory(workDir.toFile()); var exitValue = executor.execute(cmdLine); assertThat(exitValue).isZero(); } private static void assertIsOk(WsResponse response) { var code = response.code(); assertThat(code) .withFailMessage(() -> "Expected an HTTP call to have an OK code, got: " + code) // This is an approximation for "non error codes" - 200, 201, 204... + possible redirects .isBetween(200, 399); } private static MockSonarLintRpcClientDelegate newDummySonarLintClient() { return new MockSonarLintRpcClientDelegate() { @Override public Either getCredentials(String connectionId) throws ConnectionNotFoundException { if (connectionId.equals(CONNECTION_ID)) { return Either.forLeft(new TokenDto(SONARCLOUD_TOKEN)); } return super.getCredentials(connectionId); } @Override public void didChangeAnalysisReadiness(Set configurationScopeIds, boolean areReadyForAnalysis) { analysisReadinessByConfigScopeId.putAll(configurationScopeIds.stream().collect(Collectors.toMap(Function.identity(), k -> areReadyForAnalysis))); } @Override public void log(LogParams params) { System.out.println(params); rpcClientLogs.add(params); } }; } } ================================================ FILE: its/tests/src/test/java/its/SonarQubeCommunityEditionTests.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import com.sonar.orchestrator.junit5.OrchestratorExtension; import com.sonar.orchestrator.locator.FileLocation; import its.utils.OrchestratorUtils; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.users.CreateRequest; import org.sonarsource.sonarlint.core.rpc.client.ClientJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.client.ConnectionNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.SonarLintRpcClientDelegate; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.HttpConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import static java.util.Collections.emptySet; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JAVA; class SonarQubeCommunityEditionTests extends AbstractConnectedTests { private static final String CONNECTION_ID = "orchestrator"; @RegisterExtension static final OrchestratorExtension ORCHESTRATOR = OrchestratorUtils.defaultEnvBuilder() .addPlugin(FileLocation.of("../plugins/java-custom-rules/target/java-custom-rules-plugin.jar")) .setServerProperty("sonar.projectCreation.mainBranchName", MAIN_BRANCH_NAME) .build(); @TempDir private static Path sonarUserHome; private static WsClient adminWsClient; private static SonarLintRpcServer backend; private static BackendJsonRpcLauncher serverLauncher; @BeforeAll static void startBackend() throws IOException { var clientToServerOutputStream = new PipedOutputStream(); var clientToServerInputStream = new PipedInputStream(clientToServerOutputStream); var serverToClientOutputStream = new PipedOutputStream(); var serverToClientInputStream = new PipedInputStream(serverToClientOutputStream); var client = newDummySonarLintClient(); serverLauncher = new BackendJsonRpcLauncher(clientToServerInputStream, serverToClientOutputStream); var clientLauncher = new ClientJsonRpcLauncher(serverToClientInputStream, clientToServerOutputStream, client); backend = clientLauncher.getServerProxy(); try { var enabledLanguages = Set.of(JAVA); backend.initialize( new InitializeParams(IT_CLIENT_INFO, IT_TELEMETRY_ATTRIBUTES, HttpConfigurationDto.defaultConfig(), null, Set.of(), sonarUserHome.resolve("storage"), sonarUserHome.resolve("work"), Collections.emptySet(), Collections.emptyMap(), enabledLanguages, emptySet(), emptySet(), List.of(new SonarQubeConnectionConfigurationDto(CONNECTION_ID, ORCHESTRATOR.getServer().getUrl(), true)), Collections.emptyList(), sonarUserHome.toString(), Map.of(), false, null, false, null)) .get(); } catch (Exception e) { throw new IllegalStateException("Cannot initialize the backend", e); } } @BeforeAll static void createSonarLintUser() { adminWsClient = newAdminWsClient(ORCHESTRATOR); adminWsClient.users().create(new CreateRequest().setLogin(SONARLINT_USER).setPassword(SONARLINT_PWD).setName("SonarLint")); } @AfterAll static void stopBackend() throws ExecutionException, InterruptedException { serverLauncher.getServer().shutdown().get(); } private static SonarLintRpcClientDelegate newDummySonarLintClient() { return new MockSonarLintRpcClientDelegate() { @Override public Either getCredentials(String connectionId) throws ConnectionNotFoundException { if (connectionId.equals(CONNECTION_ID)) { return Either.forRight(new UsernamePasswordDto(SONARLINT_USER, SONARLINT_PWD)); } return super.getCredentials(connectionId); } @Override public void log(LogParams params) { rpcClientLogs.add(params); } }; } } ================================================ FILE: its/tests/src/test/java/its/SonarQubeDeveloperEditionTests.java ================================================ /* * SonarLint Core - ITs - Tests * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package its; import com.google.protobuf.InvalidProtocolBufferException; import com.sonar.orchestrator.build.SonarScanner; import com.sonar.orchestrator.container.Edition; import com.sonar.orchestrator.junit5.OnlyOnSonarQube; import com.sonar.orchestrator.junit5.OrchestratorExtension; import com.sonar.orchestrator.locator.FileLocation; import its.utils.OrchestratorUtils; import its.utils.PluginLocator; import java.io.File; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.mockito.ArgumentCaptor; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsClient; import org.sonarqube.ws.client.WsRequest; import org.sonarqube.ws.client.issues.DoTransitionRequest; import org.sonarqube.ws.client.issues.SearchRequest; import org.sonarqube.ws.client.settings.ResetRequest; import org.sonarqube.ws.client.settings.SetRequest; import org.sonarqube.ws.client.users.CreateRequest; import org.sonarsource.sonarlint.core.rpc.client.ClientJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.client.ConfigScopeNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.ConnectionNotFoundException; import org.sonarsource.sonarlint.core.rpc.client.SonarLintCancelChecker; import org.sonarsource.sonarlint.core.rpc.impl.BackendJsonRpcLauncher; import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.DidVcsRepositoryChangeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.branch.GetMatchedSonarProjectBranchParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.BindingConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.binding.DidUpdateBindingParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.ConfigurationScopeDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidAddConfigurationScopesParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.config.scope.DidRemoveConfigurationScopeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarQubeConnectionDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.config.SonarQubeConnectionConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.GetAllProjectsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.projects.SonarProjectDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.hotspot.HotspotStatus; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.HttpConfigurationDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.GetEffectiveRuleDetailsParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.RuleDescriptionTabDto; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.ListAllParams; import org.sonarsource.sonarlint.core.rpc.protocol.backend.tracking.TaintVulnerabilityDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.HotspotDetailsDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto; import org.sonarsource.sonarlint.core.rpc.protocol.client.log.LogParams; import org.sonarsource.sonarlint.core.rpc.protocol.client.taint.vulnerability.DidChangeTaintVulnerabilitiesParams; import org.sonarsource.sonarlint.core.rpc.protocol.common.CleanCodeAttribute; import org.sonarsource.sonarlint.core.rpc.protocol.common.Either; import org.sonarsource.sonarlint.core.rpc.protocol.common.ImpactSeverity; import org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType; import org.sonarsource.sonarlint.core.rpc.protocol.common.SoftwareQuality; import org.sonarsource.sonarlint.core.rpc.protocol.common.TextRangeDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.TokenDto; import org.sonarsource.sonarlint.core.rpc.protocol.common.UsernamePasswordDto; import static its.utils.AnalysisUtils.analyzeAndAwaitHotspots; import static its.utils.AnalysisUtils.analyzeAndAwaitIssues; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.apache.commons.lang3.StringUtils.abbreviate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.waitAtMost; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.CLOUDFORMATION; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.COBOL; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.DOCKER; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.GO; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.HTML; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JAVA; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.JS; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.KOTLIN; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.KUBERNETES; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.PHP; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.PYTHON; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.RUBY; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.SCALA; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.TERRAFORM; import static org.sonarsource.sonarlint.core.rpc.protocol.common.Language.XML; class SonarQubeDeveloperEditionTests extends AbstractConnectedTests { public static final String CONNECTION_ID = "orchestrator"; public static final String CONNECTION_ID_WRONG_CREDENTIALS = "wrong-credentials"; @RegisterExtension static OrchestratorExtension ORCHESTRATOR = OrchestratorUtils.defaultEnvBuilder() .setEdition(Edition.DEVELOPER) .activateLicense() .addPlugin(FileLocation.of("../plugins/global-extension-plugin/target/global-extension-plugin.jar")) .addPlugin(FileLocation.of("../plugins/custom-sensor-plugin/target/custom-sensor-plugin.jar")) .addPlugin(FileLocation.of("../plugins/java-custom-rules/target/java-custom-rules-plugin.jar")) // Ensure SSE are processed correctly just after SQ startup .setServerProperty("sonar.pushevents.polling.initial.delay", "2") .setServerProperty("sonar.pushevents.polling.period", "1") .setServerProperty("sonar.pushevents.polling.last.timestamp", "1") .setServerProperty("sonar.projectCreation.mainBranchName", MAIN_BRANCH_NAME) .build(); private static WsClient adminWsClient; private static MockSonarLintRpcClientDelegate client; @BeforeAll static void createSonarLintUser() { adminWsClient = newAdminWsClient(ORCHESTRATOR); adminWsClient.users().create(new CreateRequest().setLogin(SONARLINT_USER).setPassword(SONARLINT_PWD).setName("SonarLint")); } private static SonarLintRpcServer backend; private static BackendJsonRpcLauncher serverLauncher; @TempDir private static Path sonarUserHome; private static final List didChangeTaintVulnerabilitiesEvents = new CopyOnWriteArrayList<>(); private static final List allBranchNamesForProject = new CopyOnWriteArrayList<>(); private static String matchedBranchNameForProject = null; private static final List didSynchronizeConfigurationScopes = new CopyOnWriteArrayList<>(); private static final Map analysisReadinessByConfigScopeId = new ConcurrentHashMap<>(); @BeforeAll static void start() throws IOException { var clientToServerOutputStream = new PipedOutputStream(); var clientToServerInputStream = new PipedInputStream(clientToServerOutputStream); var serverToClientOutputStream = new PipedOutputStream(); var serverToClientInputStream = new PipedInputStream(serverToClientOutputStream); serverLauncher = new BackendJsonRpcLauncher(clientToServerInputStream, serverToClientOutputStream); client = spy(newDummySonarLintClient()); var clientLauncher = new ClientJsonRpcLauncher(serverToClientInputStream, clientToServerOutputStream, client); backend = clientLauncher.getServerProxy(); try { var languages = Set.of(JAVA, GO, PHP, JS, PYTHON, HTML, RUBY, KOTLIN, SCALA, XML, COBOL, CLOUDFORMATION, DOCKER, KUBERNETES, TERRAFORM); backend.initialize( new InitializeParams(IT_CLIENT_INFO, IT_TELEMETRY_ATTRIBUTES, HttpConfigurationDto.defaultConfig(), null, Set.of(BackendCapability.FULL_SYNCHRONIZATION, BackendCapability.PROJECT_SYNCHRONIZATION, BackendCapability.SERVER_SENT_EVENTS, BackendCapability.SECURITY_HOTSPOTS, BackendCapability.DATAFLOW_BUG_DETECTION), sonarUserHome.resolve("storage"), sonarUserHome.resolve("work"), emptySet(), PluginLocator.getEmbeddedPluginsByKeyForTests(), languages, emptySet(), emptySet(), List.of(new SonarQubeConnectionConfigurationDto(CONNECTION_ID, ORCHESTRATOR.getServer().getUrl(), false), new SonarQubeConnectionConfigurationDto(CONNECTION_ID_WRONG_CREDENTIALS, ORCHESTRATOR.getServer().getUrl(), false)), emptyList(), sonarUserHome.toString(), Map.of(), false, null, false, null)) .get(); } catch (Exception e) { throw new IllegalStateException("Cannot initialize the backend", e); } } @BeforeEach void clearState() { rpcClientLogs.clear(); didSynchronizeConfigurationScopes.clear(); analysisReadinessByConfigScopeId.clear(); allBranchNamesForProject.clear(); matchedBranchNameForProject = null; } @AfterAll static void stop() throws ExecutionException, InterruptedException { backend.shutdown().get(); } @Nested class AnalysisTests { @BeforeEach void start() { Map globalProps = new HashMap<>(); globalProps.put("sonar.global.label", "It works"); // This profile is altered in a test ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); } @AfterEach void stop() { adminWsClient.settings().reset(new ResetRequest().setKeys(singletonList("sonar.java.file.suffixes"))); client.clear(); } // TODO should be moved to a separate class, not related to analysis @Test void shouldRaiseIssuesOnAJavaScriptProject() { var configScopeId = "shouldRaiseIssuesOnAJavaScriptProject"; var projectKey = "sample-javascript"; provisionProject(ORCHESTRATOR, projectKey, "Sample Javascript"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/javascript-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "js", "SonarLint IT Javascript"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-javascript"), "src/Person.js"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesFromACustomRule() { var configScopeId = "shouldRaiseIssuesFromACustomRule"; var projectKey = "sample-java-custom"; provisionProject(ORCHESTRATOR, projectKey, "Sample Java Custom"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-custom.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java Custom"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java-custom"), "src/main/java/foo/Foo.java"); assertThat(rawIssues).extracting("ruleKey", "textRange") .usingRecursiveFieldByFieldElementComparator() .containsOnly(tuple("mycompany-java:AvoidAnnotation", new TextRangeDto(12, 3, 12, 19))); } @Test void shouldRaiseIssuesOnAPhpProject() { var configScopeId = "shouldRaiseIssuesOnAPhpProject"; var projectKey = "sample-php"; provisionProject(ORCHESTRATOR, projectKey, "Sample PHP"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/php-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "php", "SonarLint IT PHP"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-php"), "src/Math.php"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAPythonProject() { var configScopeId = "shouldRaiseIssuesOnAPythonProject"; var projectKey = "sample-python"; var projectName = "Sample Python"; provisionProject(ORCHESTRATOR, projectKey, projectName); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/python-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "py", "SonarLint IT Python"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-python"), "src/hello.py"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAHtmlProject() { var configScopeId = "shouldRaiseIssuesOnAHtmlProject"; var projectKey = "sample-web"; provisionProject(ORCHESTRATOR, projectKey, "Sample Web"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/web-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "web", "SonarLint IT Web"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-web"), "src/file.html"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAGoProject() { var configScopeId = "shouldRaiseIssuesOnAGoProject"; var projectKey = "sample-go"; provisionProject(ORCHESTRATOR, projectKey, "Sample Go"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/go-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "go", "SonarLint IT Go"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-go"), "src/sample.go"); // S5542 was introduced with Go Enterprise in 2025.2 var expectedIssues = ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(2025, 2) ? 2 : 1; assertThat(rawIssues).hasSize(expectedIssues); } @Test @OnlyOnSonarQube(from = "9.9") void shouldRaiseIssuesOnACloudFormationProject() { var configScopeId = "shouldRaiseIssuesOnACloudFormationProject"; var projectKey = "sample-cloudformation"; provisionProject(ORCHESTRATOR, projectKey, "Sample CloudFormation"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/cloudformation-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "cloudformation", "SonarLint IT CloudFormation"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-cloudformation"), "src/sample.yaml"); assertThat(rawIssues).hasSize(1); } @Test @OnlyOnSonarQube(from = "9.9") void shouldRaiseIssuesOnADockerProject() { var configScopeId = "shouldRaiseIssuesOnADockerProject"; var projectKey = "sample-docker"; provisionProject(ORCHESTRATOR, projectKey, "Sample Docker"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/docker-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "docker", "SonarLint IT Docker"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-docker"), "src/Dockerfile"); assertThat(rawIssues).hasSize(2); } @Test @OnlyOnSonarQube(from = "9.9") void shouldRaiseIssuesOnAKubernetesProject() { var configScopeId = "shouldRaiseIssuesOnAKubernetesProject"; var projectKey = "sample-kubernetes"; provisionProject(ORCHESTRATOR, projectKey, "Sample Kubernetes"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/kubernetes-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "kubernetes", "SonarLint IT Kubernetes"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitHotspots(backend, client, configScopeId, Path.of("projects", "sample-kubernetes"), "src/sample.yaml"); assertThat(rawIssues).hasSize(1); } @Test @OnlyOnSonarQube(from = "9.9") void shouldRaiseIssuesOnATerraformProject() { var configScopeId = "shouldRaiseIssuesOnATerraformProject"; var projectKey = "sample-terraform"; provisionProject(ORCHESTRATOR, projectKey, "Sample Terraform"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/terraform-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "terraform", "SonarLint IT Terraform"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-terraform"), "src/sample.tf"); assertThat(rawIssues).hasSize(1); } @Test @OnlyOnSonarQube(from = "10.4") void shouldRaiseDataflowIssuesOnAPythonProject() { var configScopeId = "shouldRaiseDataflowIssuesOnAPythonProject"; var projectKey = "sample-dbd"; provisionProject(ORCHESTRATOR, projectKey, "Sample DBD"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/dbd-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "py", "SonarLint IT DBD"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForSync(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-dbd"), "src/hello.py"); assertThat(rawIssues) .extracting(RaisedIssueDto::getRuleKey, RaisedIssueDto::getPrimaryMessage) .containsOnly(tuple("pythonbugs:S6466", "Fix this access on a collection that may trigger an 'IndexError'.")); } @Test void customSensorsShouldNotBeExecuted() { var configScopeId = "customSensorsShouldNotBeExecuted"; var projectKey = "sample-java-custom-sensor"; provisionProject(ORCHESTRATOR, projectKey, "Sample Java Custom"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/custom-sensor.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Custom Sensor"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); assertThat(analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java")).isEmpty(); } // TODO should be moved to a medium test @Test void globalExtension() { var configScopeId = "globalExtension"; var projectKey = "sample-global-extension"; provisionProject(ORCHESTRATOR, projectKey, "Sample Global Extension"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/global-extension.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "cobol", "SonarLint IT Global Extension"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-global-extension"), "src/foo.glob", "sonar.cobol.file.suffixes", "glob"); assertThat(rawIssues).extracting("ruleKey", "primaryMessage").containsOnly( tuple("global:inc", "Issue number 0")); rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-global-extension"), "src/foo.glob", "sonar.cobol.file.suffixes", "glob"); assertThat(rawIssues).extracting("ruleKey", "primaryMessage").containsOnly( tuple("global:inc", "Issue number 1")); } @Test void shouldRaiseIssuesFromATemplateRule() throws Exception { var configScopeId = "shouldRaiseIssuesFromATemplateRule"; var projectKey = "sample-java-template-rule"; provisionProject(ORCHESTRATOR, projectKey, "Sample Java"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); var qp = getQualityProfile(adminWsClient, "SonarLint IT Java"); PostRequest request = new PostRequest("/api/rules/create") .setParam("params", "methodName=echo;className=foo.Foo;argumentTypes=int") .setParam("name", "myrule") .setParam("severity", "MAJOR"); if (ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 0)) { request.setParam("customKey", "myrule") .setParam("markdownDescription", "my_rule_description") .setParam("templateKey", javaRuleKey("S2253")); } else { request.setParam("custom_key", "myrule") .setParam("markdown_description", "my_rule_description") .setParam("template_key", javaRuleKey("S2253")) .setParam("type", "VULNERABILITY"); } try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful()); } request = new PostRequest("/api/qualityprofiles/activate_rule") .setParam("key", qp.getKey()) .setParam("rule", javaRuleKey("myrule")); try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful(), "Unable to activate custom rule"); } try { openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(rawIssues).hasSize(3); var ruleDetails = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, javaRuleKey("myrule"), null)).get(); var details = ruleDetails.details(); assertThat(details.getDescription().getLeft().getHtmlContent()).contains("my_rule_description"); assertThat(details.getName()).isEqualTo("myrule"); if (!ORCHESTRATOR.getServer().version().isGreaterThanOrEquals(10, 0)) { assertThat(details.getType()).isEqualTo(RuleType.VULNERABILITY); } } finally { request = new PostRequest("/api/rules/delete") .setParam("key", javaRuleKey("myrule")); try (var response = adminWsClient.wsConnector().call(request)) { assertTrue(response.isSuccessful(), "Unable to delete custom rule"); } } } @Test void shouldHonorServerSideSettings() { var configScopeId = "shouldHonorServerSideSettings"; var projectKey = "sample-java-configured"; var projectName = "Sample Java"; provisionProject(ORCHESTRATOR, projectKey, projectName); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(rawIssues).hasSize(2); rpcClientLogs.clear(); didSynchronizeConfigurationScopes.clear(); // Override default file suffixes in global props so that input file is not considered as a Java file setSettingsMultiValue(null, "sonar.java.file.suffixes", ".foo"); backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, false))); await().untilAsserted(() -> assertThat(rpcClientLogs.stream().anyMatch(s -> s.getMessage().equals("Stored project analyzer configuration"))).isTrue()); assertThat(analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java")).isEmpty(); rpcClientLogs.clear(); // Override default file suffixes in project props so that input file is considered as a Java file again setSettingsMultiValue(projectKey, "sonar.java.file.suffixes", ".java"); backend.getConfigurationService().didUpdateBinding(new DidUpdateBindingParams(configScopeId, new BindingConfigurationDto(CONNECTION_ID, projectKey, true))); await().untilAsserted(() -> assertThat(rpcClientLogs.stream().anyMatch(s -> s.getMessage().equals("Stored project analyzer configuration"))).isTrue()); rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(rawIssues).hasSize(2); } @Test void shouldRaiseIssuesOnARubyProject() { var configScopeId = "shouldRaiseIssuesOnARubyProject"; var projectKey = "sample-ruby"; provisionProject(ORCHESTRATOR, projectKey, "Sample Ruby"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/ruby-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "ruby", "SonarLint IT Ruby"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-ruby"), "src/hello.rb"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAKotlinProject() { var configScopeId = "shouldRaiseIssuesOnAKotlinProject"; var projectKey = "sample-kotlin"; provisionProject(ORCHESTRATOR, projectKey, "Sample Kotlin"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/kotlin-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "kotlin", "SonarLint IT Kotlin"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-kotlin"), "src/hello.kt"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAScalaProject() { var configScopeId = "shouldRaiseIssuesOnAScalaProject"; var projectKey = "sample-scala"; provisionProject(ORCHESTRATOR, projectKey, "Sample Scala"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/scala-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "scala", "SonarLint IT Scala"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-scala"), "src/Hello.scala"); assertThat(rawIssues).hasSize(1); } @Test void shouldRaiseIssuesOnAnXmlProject() { var configScopeId = "shouldRaiseIssuesOnAnXmlProject"; var projectKey = "sample-xml"; provisionProject(ORCHESTRATOR, projectKey, "Sample XML"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/xml-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "xml", "SonarLint IT XML"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-xml"), "src/foo.xml"); assertThat(rawIssues).hasSize(1); } } @Nested class ServerSentEvents { @Test @OnlyOnSonarQube(from = "9.9") void shouldUpdateQualityProfileInLocalStorageWhenProfileChangedOnServer() { var configScopeId = "shouldUpdateQualityProfileInLocalStorageWhenProfileChangedOnServer"; var projectKey = "projectKey-sse"; provisionProject(ORCHESTRATOR, projectKey, "Sample Java"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var qualityProfile = getQualityProfile(adminWsClient, "SonarLint IT Java"); deactivateRule(adminWsClient, qualityProfile, "java:S106"); waitAtMost(1, TimeUnit.MINUTES).pollDelay(Duration.ofSeconds(10)).untilAsserted(() -> { var rawIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(rawIssues) .extracting(RaisedIssueDto::getRuleKey) .containsOnly("java:S2325"); }); } @Test void shouldUpdateIssueInLocalStorageWhenIssueResolvedOnServer() { var configScopeId = "shouldUpdateIssueInLocalStorageWhenIssueResolvedOnServer"; var projectKey = "projectKey-sse2"; provisionProject(ORCHESTRATOR, projectKey, "Sample Java"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); analyzeMavenProject("sample-java", projectKey); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var issueKey = getIssueKeys(adminWsClient, "java:S106").get(0); resolveIssueAsWontFix(adminWsClient, issueKey); waitAtMost(1, TimeUnit.MINUTES).untilAsserted(() -> { var fooIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(fooIssues) .extracting(RaisedFindingDto::getRuleKey, RaisedFindingDto::isResolved) .contains(tuple("java:S106", true)); }); } } @Nested class BranchTests { @Test void should_sync_branches_from_server() throws ExecutionException, InterruptedException { var configScopeId = "should_sync_branches_from_server"; var short_branch = "feature/short_living"; var long_branch = "branch-1.x"; var projectKey = "sample-branch"; provisionProject(ORCHESTRATOR, projectKey, "Sample Branch"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/xml-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "xml", "SonarLint IT XML"); // Use the pattern of long living branches in SQ 9.9, else we only have issues on changed files // main branch analyzeProject("sample-xml", projectKey); // short living branch analyzeProject("sample-xml", projectKey, "sonar.branch.name", short_branch); // long living branch analyzeProject("sample-xml", projectKey, "sonar.branch.name", long_branch); openBoundConfigurationScope(configScopeId, projectKey, true); waitForSync(configScopeId); var sonarProjectBranch = backend.getSonarProjectBranchService().getMatchedSonarProjectBranch(new GetMatchedSonarProjectBranchParams(configScopeId)).get(); assertThat(sonarProjectBranch.getMatchedSonarProjectBranch()).isEqualTo(MAIN_BRANCH_NAME); matchedBranchNameForProject = short_branch; backend.getSonarProjectBranchService().didVcsRepositoryChange(new DidVcsRepositoryChangeParams(configScopeId)); await().untilAsserted(() -> assertThat(backend.getSonarProjectBranchService() .getMatchedSonarProjectBranch(new GetMatchedSonarProjectBranchParams(configScopeId)) .get().getMatchedSonarProjectBranch()) .isEqualTo(short_branch)); await().untilAsserted(() -> assertThat(allBranchNamesForProject).contains(MAIN_BRANCH_NAME, short_branch, long_branch)); } @Test void should_match_issues_from_branch() { var configScopeId = "should_match_issues_from_branch"; var projectKey = "sample-java"; var projectName = "my-sample-java"; var featureBranch = "branch-1.x"; provisionProject(ORCHESTRATOR, projectKey, projectName); analyzeProject(projectKey, projectKey); analyzeProject(projectKey, projectKey, "sonar.branch.name", featureBranch); var issuesBranch = adminWsClient.issues().search(new SearchRequest().setBranch(featureBranch).setComponentKeys(List.of(projectKey))); var issueToMarkFP = issuesBranch.getIssuesList().stream().filter(issue -> issue.getRule().equals("java:S1172")).findFirst().orElseThrow(); adminWsClient.issues().doTransition(new DoTransitionRequest().setIssue(issueToMarkFP.getKey()).setTransition("falsepositive")); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var raisedIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(raisedIssues) .extracting(RaisedIssueDto::getRuleKey, RaisedIssueDto::isResolved) .contains(tuple("java:S1172", false)); didSynchronizeConfigurationScopes.clear(); matchedBranchNameForProject = featureBranch; backend.getSonarProjectBranchService().didVcsRepositoryChange(new DidVcsRepositoryChangeParams(configScopeId)); await().untilAsserted(() -> assertThat(backend.getSonarProjectBranchService() .getMatchedSonarProjectBranch(new GetMatchedSonarProjectBranchParams(configScopeId)) .get().getMatchedSonarProjectBranch()) .isEqualTo(featureBranch)); waitForSync(configScopeId); raisedIssues = analyzeAndAwaitIssues(backend, client, configScopeId, Path.of("projects", "sample-java"), "src/main/java/foo/Foo.java"); assertThat(raisedIssues) .extracting(RaisedIssueDto::getRuleKey, RaisedIssueDto::isResolved) .contains(tuple("java:S1172", true)); } } @Nested class TaintVulnerabilities { private static final String PROJECT_KEY_JAVA_TAINT = "sample-java-taint"; private static final String CONFIG_SCOPE_ID = "sample-java-taint-in-ide"; @BeforeEach void prepare() { provisionProject(ORCHESTRATOR, PROJECT_KEY_JAVA_TAINT, "Java With Taint Vulnerabilities"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint-with-taint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY_JAVA_TAINT, "java", "SonarLint Taint Java"); } @AfterEach void stop() { backend.getConfigurationService().didRemoveConfigurationScope(new DidRemoveConfigurationScopeParams(CONFIG_SCOPE_ID)); var request = new PostRequest("api/projects/bulk_delete"); request.setParam("projects", PROJECT_KEY_JAVA_TAINT); try (var response = adminWsClient.wsConnector().call(request)) { } } @Test void shouldSyncTaintVulnerabilities() throws ExecutionException, InterruptedException { openBoundConfigurationScope(CONFIG_SCOPE_ID, PROJECT_KEY_JAVA_TAINT, true); waitForAnalysisToBeReady(CONFIG_SCOPE_ID); analyzeMavenProject("sample-java-taint", PROJECT_KEY_JAVA_TAINT); // Ensure a vulnerability has been reported on server side var issuesList = adminWsClient.issues().search(new SearchRequest().setTypes(List.of("VULNERABILITY")).setComponentKeys(List.of(PROJECT_KEY_JAVA_TAINT))).getIssuesList(); assertThat(issuesList).hasSize(1); var taintVulnerabilities = backend.getTaintVulnerabilityTrackingService().listAll(new ListAllParams(CONFIG_SCOPE_ID, true)).get().getTaintVulnerabilities(); assertThat(taintVulnerabilities).hasSize(1); var taintVulnerability = taintVulnerabilities.get(0); assertThat(taintVulnerability.getTextRange().getHash()).isEqualTo(hash("statement.executeQuery(query)")); var serverVersion = ORCHESTRATOR.getServer().version(); var ruleDescriptionContextKey = serverVersion.isGreaterThanOrEquals(2025, 3) ? "java_jdbc_api" : "java_se"; assertThat(taintVulnerability.getRuleDescriptionContextKey()).isEqualTo(ruleDescriptionContextKey); if (serverVersion.isGreaterThanOrEquals(10, 8)) { assertThat(taintVulnerability.getSeverityMode().isRight()).isTrue(); // In SQ 10.8+, old MAJOR severity maps to overridden MEDIUM impact assertThat(taintVulnerability.getSeverityMode().getRight().getImpacts().get(0)).extracting("softwareQuality", "impactSeverity").containsExactly(SoftwareQuality.SECURITY, ImpactSeverity.MEDIUM); assertThat(taintVulnerability.getSeverityMode().getRight().getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.COMPLETE); } else if (serverVersion.isGreaterThanOrEquals(10, 2)) { // In 10.2 <= SQ < 10.8, the impact severity is not overridden assertThat(taintVulnerability.getSeverityMode().isRight()).isTrue(); assertThat(taintVulnerability.getSeverityMode().getRight().getImpacts().get(0)).extracting("softwareQuality", "impactSeverity").containsExactly(SoftwareQuality.SECURITY, ImpactSeverity.HIGH); assertThat(taintVulnerability.getSeverityMode().getRight().getCleanCodeAttribute()).isEqualTo(CleanCodeAttribute.COMPLETE); } else { assertThat(taintVulnerability.getSeverityMode().isLeft()).isTrue(); assertThat(taintVulnerability.getSeverityMode().getLeft().getSeverity()).isEqualTo(org.sonarsource.sonarlint.core.rpc.protocol.common.IssueSeverity.MAJOR); assertThat(taintVulnerability.getSeverityMode().getLeft().getType()).isEqualTo(org.sonarsource.sonarlint.core.rpc.protocol.common.RuleType.VULNERABILITY); } assertThat(taintVulnerability.getFlows()).isNotEmpty(); assertThat(taintVulnerability.isOnNewCode()).isTrue(); var flow = taintVulnerability.getFlows().get(0); assertThat(flow.getLocations()).isNotEmpty(); assertThat(flow.getLocations().get(0).getTextRange().getHash()).isEqualTo(hash("statement.executeQuery(query)")); assertThat(flow.getLocations().get(flow.getLocations().size() - 1).getTextRange().getHash()).isIn(hash("request.getParameter(\"user\")"), hash("request.getParameter(\"pass\")")); } @Test @OnlyOnSonarQube(from = "9.9") void shouldUpdateTaintVulnerabilityInLocalStorageWhenChangedOnServer() throws ExecutionException, InterruptedException { openBoundConfigurationScope(CONFIG_SCOPE_ID, PROJECT_KEY_JAVA_TAINT, true); waitForAnalysisToBeReady(CONFIG_SCOPE_ID); assertThat(backend.getTaintVulnerabilityTrackingService().listAll(new ListAllParams(CONFIG_SCOPE_ID)).get().getTaintVulnerabilities()).isEmpty(); // check TaintVulnerabilityRaised is received analyzeMavenProject("sample-java-taint", PROJECT_KEY_JAVA_TAINT); waitAtMost(1, TimeUnit.MINUTES).until(() -> !didChangeTaintVulnerabilitiesEvents.isEmpty()); var issues = getIssueKeys(adminWsClient, "javasecurity:S3649"); assertThat(issues).isNotEmpty(); var issueKey = issues.get(0); var firstTaintChangedEvent = didChangeTaintVulnerabilitiesEvents.remove(0); assertThat(firstTaintChangedEvent) .extracting(DidChangeTaintVulnerabilitiesParams::getConfigurationScopeId, DidChangeTaintVulnerabilitiesParams::getClosedTaintVulnerabilityIds, DidChangeTaintVulnerabilitiesParams::getUpdatedTaintVulnerabilities) .containsExactly(CONFIG_SCOPE_ID, emptySet(), emptyList()); assertThat(firstTaintChangedEvent.getAddedTaintVulnerabilities()) .extracting(TaintVulnerabilityDto::getSonarServerKey, TaintVulnerabilityDto::isResolved, TaintVulnerabilityDto::getRuleKey, TaintVulnerabilityDto::getMessage, TaintVulnerabilityDto::getIdeFilePath, TaintVulnerabilityDto::isOnNewCode) .containsExactly(tuple(issueKey, false, "javasecurity:S3649", "Change this code to not construct SQL queries directly from user-controlled data.", Paths.get("src/main/java/foo/DbHelper.java"), true)); assertThat(firstTaintChangedEvent.getAddedTaintVulnerabilities()) .flatExtracting("flows") .flatExtracting("locations") .extracting("message", "filePath", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "textRange.hash") .contains( // flow 1 (don't assert intermediate locations as they change frequently between versions) tuple("Sink: this invocation is not safe; a malicious value can be used as argument", Paths.get("src/main/java/foo/DbHelper.java"), 11, 35, 11, 64, "d123d615e9ea7cc7e78c784c768f2941"), tuple("Source: a user can craft an HTTP request with malicious content", Paths.get("src/main/java/foo/Endpoint.java"), 9, 18, 9, 46, "a2b69949119440a24e900f15c0939c30"), // flow 2 (don't assert intermediate locations as they change frequently between versions) tuple("Sink: this invocation is not safe; a malicious value can be used as argument", Paths.get("src/main/java/foo/DbHelper.java"), 11, 35, 11, 64, "d123d615e9ea7cc7e78c784c768f2941"), tuple("Source: a user can craft an HTTP request with malicious content", Paths.get("src/main/java/foo/Endpoint.java"), 8, 18, 8, 46, "2ef54227b849e317e7104dc550be8146")); var raisedIssueId = firstTaintChangedEvent.getAddedTaintVulnerabilities().get(0).getId(); var taintIssues = backend.getTaintVulnerabilityTrackingService().listAll(new ListAllParams(CONFIG_SCOPE_ID)).get().getTaintVulnerabilities(); assertThat(taintIssues) .extracting(TaintVulnerabilityDto::getSonarServerKey, TaintVulnerabilityDto::isResolved, TaintVulnerabilityDto::getRuleKey, TaintVulnerabilityDto::getMessage, TaintVulnerabilityDto::getIdeFilePath, TaintVulnerabilityDto::isOnNewCode) .containsExactly(tuple(issueKey, false, "javasecurity:S3649", "Change this code to not construct SQL queries directly from user-controlled data.", Paths.get("src/main/java/foo/DbHelper.java"), true)); assertThat(taintIssues) .flatExtracting("flows") .flatExtracting("locations") .extracting("message", "filePath", "textRange.startLine", "textRange.startLineOffset", "textRange.endLine", "textRange.endLineOffset", "textRange.hash") .contains( // flow 1 (don't assert intermediate locations as they change frequently between versions) tuple("Sink: this invocation is not safe; a malicious value can be used as argument", Paths.get("src/main/java/foo/DbHelper.java"), 11, 35, 11, 64, "d123d615e9ea7cc7e78c784c768f2941"), tuple("Source: a user can craft an HTTP request with malicious content", Paths.get("src/main/java/foo/Endpoint.java"), 9, 18, 9, 46, "a2b69949119440a24e900f15c0939c30"), // flow 2 (don't assert intermediate locations as they change frequently between versions) tuple("Sink: this invocation is not safe; a malicious value can be used as argument", Paths.get("src/main/java/foo/DbHelper.java"), 11, 35, 11, 64, "d123d615e9ea7cc7e78c784c768f2941"), tuple("Source: a user can craft an HTTP request with malicious content", Paths.get("src/main/java/foo/Endpoint.java"), 8, 18, 8, 46, "2ef54227b849e317e7104dc550be8146")); resolveIssueAsWontFix(adminWsClient, issueKey); // check IssueChangedEvent is received waitAtMost(1, TimeUnit.MINUTES).until(() -> !didChangeTaintVulnerabilitiesEvents.isEmpty()); var secondTaintEvent = didChangeTaintVulnerabilitiesEvents.remove(0); assertThat(secondTaintEvent) .extracting(DidChangeTaintVulnerabilitiesParams::getConfigurationScopeId, DidChangeTaintVulnerabilitiesParams::getClosedTaintVulnerabilityIds, DidChangeTaintVulnerabilitiesParams::getAddedTaintVulnerabilities) .containsExactly(CONFIG_SCOPE_ID, emptySet(), emptyList()); assertThat(secondTaintEvent.getUpdatedTaintVulnerabilities()) .extracting(TaintVulnerabilityDto::isResolved) .containsExactly(true); reopenIssue(adminWsClient, issueKey); // check IssueChangedEvent is received waitAtMost(1, TimeUnit.MINUTES).until(() -> !didChangeTaintVulnerabilitiesEvents.isEmpty()); var thirdTaintEvent = didChangeTaintVulnerabilitiesEvents.remove(0); assertThat(thirdTaintEvent) .extracting(DidChangeTaintVulnerabilitiesParams::getConfigurationScopeId, DidChangeTaintVulnerabilitiesParams::getClosedTaintVulnerabilityIds, DidChangeTaintVulnerabilitiesParams::getAddedTaintVulnerabilities) .containsExactly(CONFIG_SCOPE_ID, emptySet(), emptyList()); assertThat(thirdTaintEvent.getUpdatedTaintVulnerabilities()) .extracting(TaintVulnerabilityDto::isResolved) .containsExactly(false); // analyze another project under the same project key to close the taint issue analyzeMavenProject("sample-java", PROJECT_KEY_JAVA_TAINT); // check TaintVulnerabilityClosed is received waitAtMost(1, TimeUnit.MINUTES).until(() -> !didChangeTaintVulnerabilitiesEvents.isEmpty()); var fourthTaintEvent = didChangeTaintVulnerabilitiesEvents.remove(0); assertThat(fourthTaintEvent) .extracting(DidChangeTaintVulnerabilitiesParams::getConfigurationScopeId, DidChangeTaintVulnerabilitiesParams::getUpdatedTaintVulnerabilities, DidChangeTaintVulnerabilitiesParams::getAddedTaintVulnerabilities) .containsExactly(CONFIG_SCOPE_ID, emptyList(), emptyList()); assertThat(fourthTaintEvent.getClosedTaintVulnerabilityIds()) .containsExactly(raisedIssueId); } } @Nested // TODO Can be removed when switching to Java 16+ and changing prepare() to static @TestInstance(TestInstance.Lifecycle.PER_CLASS) class Hotspots { private static final String PROJECT_KEY_JAVA_HOTSPOT = "sample-java-hotspot"; @BeforeAll void prepare() { provisionProject(ORCHESTRATOR, PROJECT_KEY_JAVA_HOTSPOT, "Sample Java Hotspot"); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint-with-hotspot.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(PROJECT_KEY_JAVA_HOTSPOT, "java", "SonarLint IT Java Hotspot"); // Build project to have bytecode and analyze analyzeMavenProject("sample-java-hotspot", PROJECT_KEY_JAVA_HOTSPOT); } @BeforeEach void start() { var globalProps = new HashMap(); globalProps.put("sonar.global.label", "It works"); } @AfterEach void stop() { adminWsClient.settings().reset(new ResetRequest().setKeys(singletonList("sonar.java.file.suffixes"))); } @Test // SonarQube should support opening security hotspots @OnlyOnSonarQube(from = "9.9") @Disabled void shouldShowHotspotWhenOpenedFromSonarQube() throws InvalidProtocolBufferException { var configScopeId = "shouldShowHotspotWhenOpenedFromSonarQube"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT, true); waitForAnalysisToBeReady(configScopeId); var hotspotKey = getFirstHotspotKey(PROJECT_KEY_JAVA_HOTSPOT); requestOpenHotspotWithParams(PROJECT_KEY_JAVA_HOTSPOT, hotspotKey); var captor = ArgumentCaptor.forClass(HotspotDetailsDto.class); verify(client, timeout(1000)).showHotspot(eq(configScopeId), captor.capture()); var actualHotspot = captor.getValue(); assertThat(actualHotspot.getKey()).isEqualTo(hotspotKey); assertThat(actualHotspot.getMessage()).isEqualTo("Make sure that this logger's configuration is safe."); assertThat(actualHotspot.getIdeFilePath()).isEqualTo(Path.of("src/main/java/foo/Foo.java")); assertThat(actualHotspot.getTextRange()).usingRecursiveComparison().isEqualTo(new TextRangeDto(9, 4, 9, 45)); assertThat(actualHotspot.getAuthor()).isEmpty(); assertThat(actualHotspot.getStatus()).isEqualTo("TO_REVIEW"); assertThat(actualHotspot.getResolution()).isNull(); assertThat(actualHotspot.getRule().getKey()).isEqualTo("java:S4792"); } private int requestOpenHotspotWithParams(String projectKey, String hotspotKey) { var request = HttpRequest.newBuilder() .GET() .uri(URI.create("http://localhost:" + serverLauncher.getServer().getEmbeddedServerPort() + "/sonarlint/api/hotspots/show?server=" + URLEncoder.encode(ORCHESTRATOR.getServer().getUrl(), StandardCharsets.UTF_8) + "&project=" + projectKey + "&hotspot=" + hotspotKey)) .build(); HttpResponse response; try { response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return -1; } return response.statusCode(); } private String getFirstHotspotKey(String projectKey) throws InvalidProtocolBufferException { var response = ORCHESTRATOR.getServer() .newHttpCall("/api/hotspots/search.protobuf") .setParam("projectKey", projectKey) .setAdminCredentials() .execute(); var parser = org.sonarqube.ws.Hotspots.SearchWsResponse.parser(); return parser.parseFrom(response.getBody()).getHotspots(0).getKey(); } @Test void reportHotspots() { var configScopeId = "reportHotspots"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT, false); waitForAnalysisToBeReady(configScopeId); await().untilAsserted(() -> assertThat(rpcClientLogs.stream().anyMatch(s -> s.getMessage().equals("Stored server info"))).isTrue()); var rawIssues = analyzeAndAwaitHotspots(backend, client, configScopeId, Path.of("projects", PROJECT_KEY_JAVA_HOTSPOT), "src/main/java/foo/Foo.java", "sonar.java.binaries", new File("projects/sample-java-hotspot/target/classes").getAbsolutePath()); assertThat(rawIssues) .extracting(RaisedHotspotDto::getRuleKey, h -> h.getSeverityMode().getLeft().getType()) .containsExactly(tuple(javaRuleKey("S4792"), RuleType.SECURITY_HOTSPOT)); } @Test @OnlyOnSonarQube(from = "9.9") void loadHotspotRuleDescription() throws Exception { var configScopeId = "loadHotspotRuleDescription"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT, true); waitForAnalysisToBeReady(configScopeId); var ruleDetails = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, "java:S4792", null)).get(); assertThat(ruleDetails.details().getName()).isEqualTo("Configuring loggers is security-sensitive"); assertThat(ruleDetails.details().getDescription().getRight().getTabs().get(2).getContent().getLeft().getHtmlContent()) .contains("Check that your production deployment doesn’t have its loggers in \"debug\" mode"); } @Test void shouldMatchServerSecurityHotspots() throws InvalidProtocolBufferException { var configScopeId = "shouldMatchServerSecurityHotspots"; openBoundConfigurationScope(configScopeId, PROJECT_KEY_JAVA_HOTSPOT, true); waitForAnalysisToBeReady(configScopeId); var hotspotKey = getFirstHotspotKey(PROJECT_KEY_JAVA_HOTSPOT); resolveHotspotAsSafe(adminWsClient, hotspotKey); waitAtMost(1, TimeUnit.MINUTES).untilAsserted(() -> { // wait server event var fooIssues = analyzeAndAwaitHotspots(backend, client, configScopeId, Path.of("projects", "sample-java-hotspot"), "src/main/java/foo/Foo.java"); assertThat(fooIssues) .extracting(RaisedFindingDto::getRuleKey, RaisedHotspotDto::getStatus) .contains(tuple("java:S4792", HotspotStatus.SAFE)); }); } } @Nested class RuleDescription { @Test void shouldFailIfNotAuthenticated() { var configScopeId = "shouldFailIfNotAuthenticated"; var projectKey = "noAuth"; var projectName = "Sample Javascript"; provisionProject(ORCHESTRATOR, projectKey, projectName); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); backend.getConfigurationService().didAddConfigurationScopes(new DidAddConfigurationScopesParams( List.of(new ConfigurationScopeDto(configScopeId, null, true, projectName, new BindingConfigurationDto(CONNECTION_ID_WRONG_CREDENTIALS, projectKey, true))))); adminWsClient.settings().set(new SetRequest().setKey("sonar.forceAuthentication").setValue("true")); try { var ex = assertThrows(ExecutionException.class, () -> backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, javaRuleKey("S106"), null)).get()); assertThat(ex.getCause()).hasMessage("Could not find rule '" + javaRuleKey("S106") + "' in plugins loaded from '" + CONNECTION_ID_WRONG_CREDENTIALS + "'"); } finally { adminWsClient.settings().reset(new ResetRequest().setKeys(List.of("sonar.forceAuthentication"))); } } @Test void shouldContainExtendedDescription() throws Exception { var configScopeId = "shouldContainExtendedDescription"; var projectKey = "project-with-extended-description"; var projectName = "Project With Extended Description"; provisionProject(ORCHESTRATOR, projectKey, projectName); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java"); var extendedDescription = " = Title\n*my dummy extended description*"; WsRequest request = new PostRequest("/api/rules/update") .setParam("key", javaRuleKey("S106")) .setParam("markdown_note", extendedDescription); try (var response = adminWsClient.wsConnector().call(request)) { assertThat(response.code()).isEqualTo(200); } String expected; if (ORCHESTRATOR.getServer().version().isGreaterThan(7, 9)) { expected = "

Title

my dummy extended description"; } else { // For some reason, there is an extra line break in the generated HTML expected = "

Title\n

my dummy extended description"; } openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var ruleDetailsResponse = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, javaRuleKey("S106"), null)).get(); var ruleDescription = ruleDetailsResponse.details().getDescription(); if (ORCHESTRATOR.getServer().version().isGreaterThan(10, 1)) { var ruleTabs = ruleDescription.getRight().getTabs(); assertThat(ruleTabs.get(ruleTabs.size() - 1).getContent().getLeft().getHtmlContent()).contains(expected); } else { // no description sections at that time assertThat(ruleDescription.isRight()).isFalse(); } } @Test void shouldSupportsMarkdownDescription() throws Exception { var configScopeId = "shouldSupportsMarkdownDescription"; var projectKey = "project-with-markdown-description"; var projectName = "Project With Markdown Description"; provisionProject(ORCHESTRATOR, projectKey, projectName); ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/java-sonarlint-with-markdown.xml")); ORCHESTRATOR.getServer().associateProjectToQualityProfile(projectKey, "java", "SonarLint IT Java Markdown"); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var ruleDetailsResponse = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, "mycompany-java:markdown", null)).get(); assertThat(ruleDetailsResponse.details().getDescription().getLeft().getHtmlContent()) .isEqualTo("

Title

  • one
  • \n" + "
  • two
"); } @Test void shouldReturnAllContextsWithOthersSelectedIfNoContextProvided() throws ExecutionException, InterruptedException { var configScopeId = "shouldReturnAllContextsWithOthersSelectedIfNoContextProvided"; var projectKey = "sample-java-taint-new-backend"; var projectName = "Java With Taint Vulnerabilities"; provisionProject(ORCHESTRATOR, projectKey, projectName); openBoundConfigurationScope(configScopeId, projectKey, true); waitForAnalysisToBeReady(configScopeId); var activeRuleDetailsResponse = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams(configScopeId, "javasecurity:S2083", null)).get(); var description = activeRuleDetailsResponse.details().getDescription(); var serverVersion = ORCHESTRATOR.getServer().version(); var extendedDescription = description.getRight(); assertThat(extendedDescription.getIntroductionHtmlContent()).isNull(); var framework = serverVersion.isGreaterThanOrEquals(2025, 3) ? "Java I/O API (java_i_o_api)" : "Java SE (java_se)"; var link = serverVersion.isGreaterThanOrEquals(2026, 2) ? "OWASP - Path injections occur when an application us...", "How can I fix it?", "--> " + framework, "

The following code is vulnerable to path inj...", "--> Others (others)", "

How can I fix it in another component or fr...", "More Info", "

Standards

\n" + "