diff --git a/src/CFamily.UnitTests/Analysis/CLangAnalyzerTests.cs b/src/CFamily.UnitTests/Analysis/CLangAnalyzerTests.cs deleted file mode 100644 index 1820337fde..0000000000 --- a/src/CFamily.UnitTests/Analysis/CLangAnalyzerTests.cs +++ /dev/null @@ -1,390 +0,0 @@ -/* - * SonarLint for Visual Studio - * Copyright (C) 2016-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System; -using System.IO.Abstractions; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.CFamily.Helpers.UnitTests; -using SonarLint.VisualStudio.CFamily.Rules; -using SonarLint.VisualStudio.CFamily.SubProcess; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Integration; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.CFamily.Analysis.UnitTests -{ - [TestClass] - public class CLangAnalyzerTests - { - private static readonly IIssueConsumer ValidIssueConsumer = Mock.Of(); - private static readonly IAnalysisStatusNotifier AnyStatusNotifier = Mock.Of(); - - [TestMethod] - public void IsSupported() - { - var testSubject = new CLangAnalyzer(Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of()); - - testSubject.IsAnalysisSupported(new[] { AnalysisLanguage.CFamily }).Should().BeTrue(); - testSubject.IsAnalysisSupported(new[] { AnalysisLanguage.Javascript }).Should().BeFalse(); - testSubject.IsAnalysisSupported(new[] { AnalysisLanguage.CascadingStyleSheets }).Should().BeFalse(); - testSubject.IsAnalysisSupported(new[] { AnalysisLanguage.Javascript, AnalysisLanguage.CFamily }).Should().BeTrue(); - } - - [TestMethod] - public async Task ExecuteAnalysis_RequestCannotBeCreated_NoAnalysis() - { - var analysisOptions = new CFamilyAnalyzerOptions(); - var requestFactory = CreateRequestFactory("path", analysisOptions, null); - - var testSubject = CreateTestableAnalyzer(requestFactory: requestFactory.Object); - await testSubject.TriggerAnalysisAsync("path", new[] { AnalysisLanguage.CFamily }, ValidIssueConsumer, analysisOptions, AnyStatusNotifier, CancellationToken.None); - - requestFactory.Verify(x => x.TryCreateAsync("path", analysisOptions), Times.Once); - - // TODO - modify check to be more reliable - Thread.Sleep(400); // delay in case the background thread has gone on to call the subprocess - testSubject.SubProcessExecutedCount.Should().Be(0); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public async Task ExecuteAnalysis_RequestCannotBeCreated_NotPCH_LogOutput(bool isNullOptions) - { - var analysisOptions = isNullOptions ? null : new CFamilyAnalyzerOptions { CreatePreCompiledHeaders = false }; - var requestFactory = CreateRequestFactory("path", analysisOptions, null); - var testLogger = new TestLogger(); - - var testSubject = CreateTestableAnalyzer( - requestFactory: requestFactory.Object, - logger: testLogger); - - await testSubject.TriggerAnalysisAsync("path", new[] { AnalysisLanguage.CFamily }, ValidIssueConsumer, analysisOptions, AnyStatusNotifier, CancellationToken.None); - - testLogger.AssertOutputStringExists(string.Format(CFamilyStrings.MSG_UnableToCreateConfig, "path")); - } - - [TestMethod] - public async Task ExecuteAnalysis_RequestCannotBeCreated_PCH_NoLogOutput() - { - var analysisOptions = new CFamilyAnalyzerOptions {CreatePreCompiledHeaders = true}; - var requestFactory = CreateRequestFactory("path", analysisOptions, null); - var testLogger = new TestLogger(); - - var testSubject = CreateTestableAnalyzer( - requestFactory: requestFactory.Object, - logger: testLogger); - - await testSubject.TriggerAnalysisAsync("path", new[] { AnalysisLanguage.CFamily }, ValidIssueConsumer, analysisOptions, AnyStatusNotifier, CancellationToken.None); - - testLogger.AssertNoOutputMessages(); - } - - [TestMethod] - public async Task ExecuteAnalysis_RequestCanBeCreated_AnalysisIsTriggered() - { - var analysisOptions = new CFamilyAnalyzerOptions(); - var request = CreateRequest("path"); - var requestFactory = CreateRequestFactory("path", analysisOptions, request); - - - var testSubject = CreateTestableAnalyzer( - requestFactory: requestFactory.Object); - - await testSubject.TriggerAnalysisAsync("path", new[] { AnalysisLanguage.CFamily }, ValidIssueConsumer, analysisOptions, AnyStatusNotifier, CancellationToken.None); - - testSubject.SubProcessExecutedCount.Should().Be(1); - } - - [TestMethod] - public async Task TriggerAnalysisAsync_StreamsIssuesFromSubProcessToConsumer() - { - const string fileName = "c:\\data\\aaa\\bbb\\file.txt"; - var rulesConfig = new DummyCFamilyRulesConfig("c") - .AddRule("rule1", isActive: true) - .AddRule("rule2", isActive: true); - - var request = CreateRequest - ( - file: fileName, - rulesConfiguration: rulesConfig, - language: rulesConfig.LanguageKey - ); - var requestFactory = CreateRequestFactory(fileName, ValidAnalyzerOptions, request); - - var message1 = new Message("rule1", fileName, 1, 1, 1, 1, "message one", false, Array.Empty(), Array.Empty()); - var message2 = new Message("rule2", fileName, 2, 2, 2, 2, "message two", false, Array.Empty(), Array.Empty()); - - var convertedMessage1 = Mock.Of(); - var convertedMessage2 = Mock.Of(); - - var issueConverter = new Mock(); - issueConverter - .Setup(x => x.Convert(message1, request.Context.CFamilyLanguage, rulesConfig)) - .Returns(convertedMessage1); - - issueConverter - .Setup(x => x.Convert(message2, request.Context.CFamilyLanguage, rulesConfig)) - .Returns(convertedMessage2); - - var issueConverterFactory = new Mock(); - issueConverterFactory.Setup(x => x.Create()).Returns(issueConverter.Object); - - var mockConsumer = new Mock(); - var statusNotifier = new Mock(); - - var testSubject = CreateTestableAnalyzer(issueConverterFactory: issueConverterFactory.Object, - requestFactory: requestFactory.Object); - - TestableCLangAnalyzer.HandleCallSubProcess subProcessOp = (handleMessage, _, _, _, _) => - { - // NOTE: on a background thread so the assertions might be handled by the product code. - // Must check testSubject.SubProcessCompleted on the "main" test thread. - - // Stream the first message to the analyzer - handleMessage(message1); - - mockConsumer.Verify(x => x.Accept(fileName, It.IsAny>()), Times.Once); - var suppliedIssues = (IEnumerable)mockConsumer.Invocations[0].Arguments[1]; - suppliedIssues.Count().Should().Be(1); - suppliedIssues.First().Should().Be(convertedMessage1); - - // Stream the second message to the analyzer - handleMessage(message2); - - mockConsumer.Verify(x => x.Accept(fileName, It.IsAny>()), Times.Exactly(2)); - suppliedIssues = (IEnumerable)mockConsumer.Invocations[1].Arguments[1]; - suppliedIssues.Count().Should().Be(1); - suppliedIssues.First().Should().Be(convertedMessage2); - }; - testSubject.SetCallSubProcessBehaviour(subProcessOp); - - await testSubject.TriggerAnalysisAsync(fileName, ValidDetectedLanguages, mockConsumer.Object, ValidAnalyzerOptions, statusNotifier.Object, CancellationToken.None); - - testSubject.SubProcessCompleted.Should().BeTrue(); - - statusNotifier.Verify(x => x.AnalysisStarted(), Times.Once); - statusNotifier.Verify(x => x.AnalysisFinished(2, It.IsAny()), Times.Once); - statusNotifier.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task TriggerAnalysisAsync_AnalysisIsCancelled_NotifiesOfCancellation() - { - var mockConsumer = new Mock(); - var originalStatusNotifier = new Mock(); - - // Call the CLangAnalyzer on another thread (that thread is blocked by subprocess wrapper) - var filePath = "c:\\test.cpp"; - var request = CreateRequest(filePath); - var requestFactory = CreateRequestFactory(filePath, ValidAnalyzerOptions, request); - - var testSubject = CreateTestableAnalyzer(requestFactory: requestFactory.Object); - - using var cts = new CancellationTokenSource(); - - TestableCLangAnalyzer.HandleCallSubProcess subProcessAction = (_, _, _, _, _) => - { - cts.Cancel(); - }; - testSubject.SetCallSubProcessBehaviour(subProcessAction); - - // Expecting to use this status notifier, not the one supplied in the constructor - var statusNotifier = new Mock(); - - await testSubject.TriggerAnalysisAsync(filePath, ValidDetectedLanguages, mockConsumer.Object, ValidAnalyzerOptions, statusNotifier.Object, cts.Token); - - testSubject.SubProcessCompleted.Should().BeTrue(); - - statusNotifier.Verify(x => x.AnalysisStarted(), Times.Once); - statusNotifier.Verify(x => x.AnalysisCancelled(), Times.Once); - statusNotifier.VerifyNoOtherCalls(); - originalStatusNotifier.Invocations.Count.Should().Be(0); - } - - [TestMethod] - public async Task TriggerAnalysisAsync_AnalysisFailsDueToException_NotifiesOfFailure() - { - void MockSubProcessCall(Action message, IRequest request, ISonarLintSettings settings, ILogger logger, CancellationToken token) - { - throw new NullReferenceException("test"); - } - - var statusNotifier = new Mock(); - - var filePath = "c:\\test.cpp"; - var request = CreateRequest(filePath); - var requestFactory = CreateRequestFactory(filePath, ValidAnalyzerOptions, request); - - var testSubject = CreateTestableAnalyzer(requestFactory: requestFactory.Object); - testSubject.SetCallSubProcessBehaviour(MockSubProcessCall); - - await testSubject.TriggerAnalysisAsync(filePath, ValidDetectedLanguages, ValidIssueConsumer, ValidAnalyzerOptions, statusNotifier.Object, CancellationToken.None); - - statusNotifier.Verify(x => x.AnalysisStarted(), Times.Once); - statusNotifier.Verify(x => x.AnalysisFailed(It.Is(e => e.Message == "test")), Times.Once); - statusNotifier.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task TriggerAnalysisAsync_AnalysisFailsDueToInternalMessage_NotifiesOfFailure() - { - const string fileName = "c:\\data\\aaa\\bbb\\file.txt"; - var request = CreateRequest(fileName); - var requestFactory = CreateRequestFactory(fileName, ValidAnalyzerOptions, request); - - var internalErrorMessage = new Message("internal.UnexpectedFailure", "", 1, 1, 1, 1, "XXX Error in subprocess XXX", false, Array.Empty(), Array.Empty()); - - var issueConverterFactory = Mock.Of(); - var mockConsumer = new Mock(); - var statusNotifier = new Mock(); - - var testSubject = CreateTestableAnalyzer(issueConverterFactory: issueConverterFactory, - requestFactory: requestFactory.Object); - - TestableCLangAnalyzer.HandleCallSubProcess subProcessOp = (handleMessage, _, _, _, _) => - { - handleMessage(internalErrorMessage); - }; - testSubject.SetCallSubProcessBehaviour(subProcessOp); - - await testSubject.TriggerAnalysisAsync(fileName, ValidDetectedLanguages, mockConsumer.Object, ValidAnalyzerOptions, statusNotifier.Object, CancellationToken.None); - - testSubject.SubProcessCompleted.Should().BeTrue(); - - statusNotifier.Verify(x => x.AnalysisStarted(), Times.Once); - statusNotifier.Verify(x => x.AnalysisFailed(CFamilyStrings.MSG_GenericAnalysisFailed), Times.Once); - statusNotifier.VerifyNoOtherCalls(); - } - - [TestMethod] - public async Task TriggerAnalysisAsync_SwitchesToBackgroundThreadBeforeProcessing() - { - var callOrder = new List(); - - var threadHandling = new Mock(); - threadHandling.Setup(x => x.SwitchToBackgroundThread()) - .Returns(() => new NoOpThreadHandler.NoOpAwaitable()) - .Callback(() => callOrder.Add("SwitchToBackgroundThread")); - - var requestFactory = new Mock(); - requestFactory.Setup(x => x.TryCreateAsync(It.IsAny(), It.IsAny())) - .Callback(() => callOrder.Add("TryCreateAsync")); - - var testSubject = CreateTestableAnalyzer(requestFactory: requestFactory.Object, threadHandling: threadHandling.Object); - await testSubject.TriggerAnalysisAsync("path", ValidDetectedLanguages, Mock.Of(), - Mock.Of(), Mock.Of(), CancellationToken.None); - - callOrder.Should().Equal("SwitchToBackgroundThread", "TryCreateAsync"); - } - - private readonly AnalysisLanguage[] ValidDetectedLanguages = new[] { AnalysisLanguage.CFamily }; - private readonly CFamilyAnalyzerOptions ValidAnalyzerOptions = null; - - private static IRequest CreateRequest(string file = null, string language = null, ICFamilyRulesConfig rulesConfiguration = null) - { - var request = new Mock(); - var context = new RequestContext(language, rulesConfiguration, file, null, null, false); - request.SetupGet(x => x.Context).Returns(context); - return request.Object; - } - - private static Mock CreateRequestFactory(string filePath, CFamilyAnalyzerOptions analysisOptions, IRequest request) - { - var factory = new Mock(); - factory.Setup(x => x.TryCreateAsync(filePath, analysisOptions)) - .Returns(Task.FromResult(request)); - return factory; - } - - private static TestableCLangAnalyzer CreateTestableAnalyzer(ITelemetryManager telemetryManager = null, - ISonarLintSettings settings = null, - ICFamilyIssueConverterFactory issueConverterFactory = null, - IRequestFactoryAggregate requestFactory = null, - ILogger logger = null, - IFileSystem fileSystem = null, - IThreadHandling threadHandling = null) - { - telemetryManager ??= Mock.Of(); - settings ??= new ConfigurableSonarLintSettings(); - issueConverterFactory ??= Mock.Of(); - requestFactory ??= Mock.Of(); - logger ??= new TestLogger(); - fileSystem ??= Mock.Of(); - threadHandling ??= new NoOpThreadHandler(); - - return new TestableCLangAnalyzer(telemetryManager, settings, logger, issueConverterFactory, requestFactory, fileSystem, threadHandling); - } - - private class TestableCLangAnalyzer : CLangAnalyzer - { - public delegate void HandleCallSubProcess(Action handleMessage, IRequest request, - ISonarLintSettings settings, ILogger logger, CancellationToken cancellationToken); - - private HandleCallSubProcess onCallSubProcess; - - public void SetCallSubProcessBehaviour(HandleCallSubProcess onCallSubProcess) => - this.onCallSubProcess = onCallSubProcess; - - public bool SubProcessCompleted { get; private set; } - - public int SubProcessExecutedCount { get; private set; } - - public TestableCLangAnalyzer(ITelemetryManager telemetryManager, ISonarLintSettings settings, - ILogger logger, - ICFamilyIssueConverterFactory cFamilyIssueConverterFactory, IRequestFactoryAggregate requestFactory, IFileSystem fileSystem, - IThreadHandling threadHandling) - : base(telemetryManager, settings, Mock.Of(), cFamilyIssueConverterFactory, requestFactory, logger, fileSystem, threadHandling) - { } - - protected override void CallSubProcess(Action handleMessage, IRequest request, - ISonarLintSettings settings, ILogger logger, CancellationToken cancellationToken) - { - SubProcessExecutedCount++; - if (onCallSubProcess == null) - { - base.CallSubProcess(handleMessage, request, settings, logger, cancellationToken); - } - else - { - onCallSubProcess(handleMessage, request, settings, logger, cancellationToken); - - // The sub process is executed on a separate thread, so any exceptions might be - // squashed by the product code. So, we'll set a flag to indicate whether it - // ran to completion. - SubProcessCompleted = true; - } - } - } - } -} diff --git a/src/CFamily/Analysis/CLangAnalyzer.cs b/src/CFamily/Analysis/CLangAnalyzer.cs index 2cd7d3e9a2..2fc99a3cd0 100644 --- a/src/CFamily/Analysis/CLangAnalyzer.cs +++ b/src/CFamily/Analysis/CLangAnalyzer.cs @@ -19,16 +19,7 @@ */ using System.ComponentModel.Composition; -using System.IO; -using System.IO.Abstractions; -using Microsoft.VisualStudio.Threading; -using SonarLint.VisualStudio.CFamily.SubProcess; -using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Analysis; -using SonarLint.VisualStudio.Core.Telemetry; -using SonarLint.VisualStudio.Infrastructure.VS; -using SonarLint.VisualStudio.Integration; -using Task = System.Threading.Tasks.Task; namespace SonarLint.VisualStudio.CFamily.Analysis { @@ -42,49 +33,13 @@ void ExecuteAnalysis(string path, CancellationToken cancellationToken); } - [Export(typeof(IAnalyzer))] [Export(typeof(ICFamilyAnalyzer))] [PartCreationPolicy(CreationPolicy.Shared)] internal class CLangAnalyzer : ICFamilyAnalyzer { - private readonly ITelemetryManager telemetryManager; - private readonly ISonarLintSettings settings; - private readonly IAnalysisStatusNotifierFactory analysisStatusNotifierFactory; - private readonly ILogger logger; - private readonly ICFamilyIssueConverterFactory issueConverterFactory; - private readonly IRequestFactoryAggregate requestFactory; - private readonly IFileSystem fileSystem; - private readonly IThreadHandling threadHandling; - [ImportingConstructor] - public CLangAnalyzer(ITelemetryManager telemetryManager, - ISonarLintSettings settings, - IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, - ICFamilyIssueConverterFactory issueConverterFactory, - IRequestFactoryAggregate requestFactory, - ILogger logger) - : this(telemetryManager, settings, analysisStatusNotifierFactory, issueConverterFactory, requestFactory, logger, new FileSystem(), ThreadHandling.Instance) - { - } - - internal /* for testing */ CLangAnalyzer(ITelemetryManager telemetryManager, - ISonarLintSettings settings, - IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, - ICFamilyIssueConverterFactory issueConverterFactory, - IRequestFactoryAggregate requestFactory, - ILogger logger, - IFileSystem fileSystem, - IThreadHandling threadHandling) - + public CLangAnalyzer() { - this.telemetryManager = telemetryManager; - this.settings = settings; - this.analysisStatusNotifierFactory = analysisStatusNotifierFactory; - this.logger = logger; - this.issueConverterFactory = issueConverterFactory; - this.requestFactory = requestFactory; - this.fileSystem = fileSystem; - this.threadHandling = threadHandling; } public bool IsAnalysisSupported(IEnumerable languages) @@ -92,170 +47,24 @@ public bool IsAnalysisSupported(IEnumerable languages) return languages.Contains(AnalysisLanguage.CFamily); } - public void ExecuteAnalysis(string path, Guid analysisId, IEnumerable detectedLanguages, - IIssueConsumer consumer, IAnalyzerOptions analyzerOptions, - CancellationToken cancellationToken) - { - var analysisStatusNotifier = analysisStatusNotifierFactory.Create(nameof(CLangAnalyzer), path); - - ExecuteAnalysis(path, detectedLanguages, consumer, analyzerOptions, analysisStatusNotifier, cancellationToken); - } - - public void ExecuteAnalysis(string path, + public void ExecuteAnalysis( + string path, + Guid analysisId, IEnumerable detectedLanguages, IIssueConsumer consumer, IAnalyzerOptions analyzerOptions, - IAnalysisStatusNotifier statusNotifier, - CancellationToken cancellationToken) => - TriggerAnalysisAsync(path, detectedLanguages, consumer, analyzerOptions, statusNotifier, cancellationToken) - .Forget(); // fire and forget + CancellationToken cancellationToken) + { + } - internal /* for testing */ async Task TriggerAnalysisAsync(string path, + public void ExecuteAnalysis( + string path, IEnumerable detectedLanguages, IIssueConsumer consumer, IAnalyzerOptions analyzerOptions, IAnalysisStatusNotifier statusNotifier, CancellationToken cancellationToken) { - Debug.Assert(IsAnalysisSupported(detectedLanguages)); - - // Switch to a background thread - await threadHandling.SwitchToBackgroundThread(); - - var request = await TryCreateRequestAsync(path, analyzerOptions); - - if (request != null) - { - RunAnalysis(request, consumer, statusNotifier, cancellationToken); - } - } - - private async Task TryCreateRequestAsync(string path, IAnalyzerOptions analyzerOptions) - { - var cFamilyAnalyzerOptions = analyzerOptions as CFamilyAnalyzerOptions; - var request = await requestFactory.TryCreateAsync(path, cFamilyAnalyzerOptions); - - if (request == null) - { - // Logging for PCH is too noisy: #2553 - if (cFamilyAnalyzerOptions == null || !cFamilyAnalyzerOptions.CreatePreCompiledHeaders) - { - logger.WriteLine(CFamilyStrings.MSG_UnableToCreateConfig, path); - } - return null; - } - - return request; - } - - protected /* for testing */ virtual void CallSubProcess(Action handleMessage, IRequest request, ISonarLintSettings settings, ILogger logger, CancellationToken cancellationToken) - { - ExecuteSubProcess(handleMessage, request, new ProcessRunner(settings, logger), logger, cancellationToken, fileSystem); - } - - private void RunAnalysis(IRequest request, IIssueConsumer consumer, IAnalysisStatusNotifier statusNotifier, CancellationToken cancellationToken) - { - var analysisStopwatch = Stopwatch.StartNew(); - statusNotifier?.AnalysisStarted(); - - var messageHandler = consumer == null - ? NoOpMessageHandler.Instance - : new MessageHandler(request, consumer, issueConverterFactory.Create(), logger); - - try - { - // We're tying up a background thread waiting for out-of-process analysis. We could - // change the process runner so it works asynchronously. Alternatively, we could change the - // RequestAnalysis method to be asynchronous, rather than fire-and-forget. - CallSubProcess(messageHandler.HandleMessage, request, settings, logger, cancellationToken); - - if (cancellationToken.IsCancellationRequested) - { - statusNotifier?.AnalysisCancelled(); - } - else - { - if (messageHandler.AnalysisSucceeded) - { - statusNotifier?.AnalysisFinished(messageHandler.IssueCount, analysisStopwatch.Elapsed); - } - else - { - statusNotifier?.AnalysisFailed(CFamilyStrings.MSG_GenericAnalysisFailed); - } - } - } - catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) - { - statusNotifier?.AnalysisFailed(ex); - } - - telemetryManager.LanguageAnalyzed(request.Context.CFamilyLanguage, analysisStopwatch.Elapsed); // different keys for C and C++ - } - - internal /* for testing */ static void ExecuteSubProcess(Action handleMessage, IRequest request, IProcessRunner runner, ILogger logger, CancellationToken cancellationToken, IFileSystem fileSystem) - { - if (SubProcessFilePaths.AnalyzerExeFilePath == null) - { - logger.WriteLine(CFamilyStrings.MSG_UnableToLocateSubProcessExe); - return; - } - - var createReproducer = request.Context.AnalyzerOptions?.CreateReproducer ?? false; - if (createReproducer) - { - SaveRequestDiagnostics(request, logger, fileSystem); - } - - const string communicateViaStreaming = "-"; // signal the subprocess we want to communicate via standard IO streams. - - var args = new ProcessRunnerArguments(SubProcessFilePaths.AnalyzerExeFilePath, false) - { - CmdLineArgs = new[] { communicateViaStreaming }, - CancellationToken = cancellationToken, - WorkingDirectory = SubProcessFilePaths.WorkingDirectory, - EnvironmentVariables = request.EnvironmentVariables, - HandleInputStream = writer => - { - using (var binaryWriter = new BinaryWriter(writer.BaseStream)) - { - request.WriteRequest(binaryWriter); - } - }, - HandleOutputStream = reader => - { - if (createReproducer) - { - reader.ReadToEnd(); - logger.WriteLine(CFamilyStrings.MSG_ReproducerSaved, SubProcessFilePaths.ReproducerFilePath); - } - else if (request.Context.AnalyzerOptions?.CreatePreCompiledHeaders ?? false) - { - reader.ReadToEnd(); - logger.WriteLine(CFamilyStrings.MSG_PchSaved, request.Context.File, request.Context.PchFile); - } - else - { - using (var binaryReader = new BinaryReader(reader.BaseStream)) - { - Protocol.Read(binaryReader, handleMessage); - } - } - } - }; - - runner.Execute(args); - } - - private static void SaveRequestDiagnostics(IRequest request, ILogger logger, IFileSystem fileSystem) - { - using (var stream = fileSystem.FileStream.Create(SubProcessFilePaths.RequestConfigFilePath, FileMode.Create, FileAccess.Write)) - using (var writer = new StreamWriter(stream)) - { - request.WriteRequestDiagnostics(writer); - } - - logger.WriteLine(CFamilyStrings.MSG_RequestConfigSaved, SubProcessFilePaths.RequestConfigFilePath); } } } diff --git a/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs b/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs index 0ee28cef34..c1a57476b6 100644 --- a/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs +++ b/src/Integration.UnitTests/SLCore/SLCoreConstantsProviderTests.cs @@ -149,6 +149,8 @@ public void AnalyzableLanguages_ShouldBeExpected() Language.JS, Language.TS, Language.CSS, + Language.C, + Language.CPP, Language.SECRETS }; diff --git a/src/Integration/SLCore/SLCoreConstantsProvider.cs b/src/Integration/SLCore/SLCoreConstantsProvider.cs index 5cdd32e317..37600dc6df 100644 --- a/src/Integration/SLCore/SLCoreConstantsProvider.cs +++ b/src/Integration/SLCore/SLCoreConstantsProvider.cs @@ -66,6 +66,8 @@ public SLCoreConstantsProvider(IVsInfoProvider vsInfoProvider) Language.JS, Language.TS, Language.CSS, + Language.C, + Language.CPP, Language.SECRETS ]; diff --git a/src/SLCore.UnitTests/Analysis/SLCoreAnalyzerTests.cs b/src/SLCore.UnitTests/Analysis/SLCoreAnalyzerTests.cs index a13d1ed1ca..3f97c8ddee 100644 --- a/src/SLCore.UnitTests/Analysis/SLCoreAnalyzerTests.cs +++ b/src/SLCore.UnitTests/Analysis/SLCoreAnalyzerTests.cs @@ -20,6 +20,7 @@ using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Core.CFamily; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.SLCore.Analysis; @@ -38,8 +39,9 @@ public void MefCtor_CheckIsExported() MefTestHelpers.CheckTypeCanBeImported( MefTestHelpers.CreateExport(), MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport(), - MefTestHelpers.CreateExport()); + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport(), + MefTestHelpers.CreateExport()); } [TestMethod] @@ -47,7 +49,7 @@ public void MefCtor_CheckIsSingleton() { MefTestHelpers.CheckIsSingletonMefComponent(); } - + [TestMethod] public void IsAnalysisSupported_ReturnsTrueForNoDetectedLanguage() { @@ -55,7 +57,7 @@ public void IsAnalysisSupported_ReturnsTrueForNoDetectedLanguage() testSubject.IsAnalysisSupported([]).Should().BeTrue(); } - + [DataTestMethod] [DataRow(AnalysisLanguage.Javascript)] [DataRow(AnalysisLanguage.TypeScript)] @@ -74,54 +76,54 @@ public void ExecuteAnalysis_CreatesNotifierAndStarts() { var analysisStatusNotifierFactory = CreateDefaultAnalysisStatusNotifier(out var notifier); var testSubject = CreateTestSubject(analysisStatusNotifierFactory: analysisStatusNotifierFactory); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); analysisStatusNotifierFactory.Received().Create(nameof(SLCoreAnalyzer), @"C:\file\path"); notifier.Received().AnalysisStarted(); } - + [TestMethod] public void ExecuteAnalysis_ConfigScopeNotInitialized_NotifyNotReady() { var activeConfigScopeTracker = Substitute.For(); activeConfigScopeTracker.Current.Returns((ConfigurationScope)null); var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), activeConfigScopeTracker, CreateDefaultAnalysisStatusNotifier(out var notifier)); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); _ = activeConfigScopeTracker.Received().Current; analysisService.ReceivedCalls().Should().BeEmpty(); notifier.Received().AnalysisNotReady(SLCoreStrings.ConfigScopeNotInitialized); } - + [TestMethod] public void ExecuteAnalysis_ConfigScopeNotReadyForAnalysis_NotifyNotReady() { var activeConfigScopeTracker = Substitute.For(); activeConfigScopeTracker.Current.Returns(new ConfigurationScope("someconfigscopeid", IsReadyForAnalysis: false)); var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), activeConfigScopeTracker, CreateDefaultAnalysisStatusNotifier(out var notifier)); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); _ = activeConfigScopeTracker.Received().Current; analysisService.ReceivedCalls().Should().BeEmpty(); notifier.Received().AnalysisNotReady(SLCoreStrings.ConfigScopeNotInitialized); } - + [TestMethod] public void ExecuteAnalysis_ServiceProviderUnavailable_NotifyFailed() { var slCoreServiceProvider = CreatServiceProvider(out var analysisService, false); var testSubject = CreateTestSubject(slCoreServiceProvider, CreateInitializedConfigScope("someconfigscopeid"), CreateDefaultAnalysisStatusNotifier(out var notifier)); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); slCoreServiceProvider.Received().TryGetTransientService(out Arg.Any()); analysisService.ReceivedCalls().Should().BeEmpty(); notifier.Received().AnalysisFailed(SLCoreStrings.ServiceProviderNotInitialized); } - + [TestMethod] public void ExecuteAnalysis_PassesCorrectArgumentsToAnalysisService() { @@ -131,15 +133,15 @@ public void ExecuteAnalysis_PassesCorrectArgumentsToAnalysisService() testSubject.ExecuteAnalysis(@"C:\file\path", analysisId, default, default, default, default); - analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Is(a => - a.analysisId == analysisId + analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Is(a => + a.analysisId == analysisId && a.configurationScopeId == "someconfigscopeid" && a.filesToAnalyze.Single() == new FileUri(@"C:\file\path") - && a.extraProperties != null + && a.extraProperties.Count == 0 && a.startTime == expectedTimeStamp.ToUnixTimeMilliseconds()), Arg.Any()); } - + [DataTestMethod] [DataRow(null, false)] [DataRow(false, false)] @@ -150,12 +152,41 @@ public void ExecuteAnalysis_ShouldFetchServerIssues_PassesCorrectValueToAnalysis var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid")); testSubject.ExecuteAnalysis(@"C:\file\path", default, default, default, options, default); - - analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Is(a => + + analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Is(a => a.shouldFetchServerIssues == expected), Arg.Any()); } + [TestMethod] + public void ExecuteAnalysis_ForCFamily_PassesCompilationDatabaseAsExtraProperties() + { + const string compilationDatabasePath = @"C:\file\path\compilation_database.json"; + var compilationDatabaseLocator = WithCompilationDatabase(compilationDatabasePath); + var activeConfigScopeTracker = CreateInitializedConfigScope("someconfigscopeid"); + var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), activeConfigScopeTracker, compilationDatabaseLocator: compilationDatabaseLocator); + + testSubject.ExecuteAnalysis(@"C:\file\path\myclass.cpp", Guid.NewGuid(), [AnalysisLanguage.CFamily], default, default, default); + + analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Is(a => + a.extraProperties != null + && a.extraProperties["sonar.cfamily.compile-commands"] == compilationDatabasePath), + Arg.Any()); + } + + [TestMethod] + public void ExecuteAnalysis_ForCFamily_WithoutCompilationDatabase_FailsToAnalyze() + { + var compilationDatabaseLocator = WithCompilationDatabase(null); + var activeConfigScopeTracker = CreateInitializedConfigScope("someconfigscopeid"); + var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), activeConfigScopeTracker, CreateDefaultAnalysisStatusNotifier(out var notifier), compilationDatabaseLocator: compilationDatabaseLocator); + + testSubject.ExecuteAnalysis(@"C:\file\path\myclass.cpp", Guid.NewGuid(), [AnalysisLanguage.CFamily], default, default, default); + + analysisService.ReceivedCalls().Should().BeEmpty(); + notifier.Received().AnalysisFailed(SLCoreStrings.CompilationDatabaseNotFound); + } + [TestMethod] public void ExecuteAnalysis_PassesCorrectCancellationTokenToAnalysisService() { @@ -164,57 +195,57 @@ public void ExecuteAnalysis_PassesCorrectCancellationTokenToAnalysisService() var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid")); testSubject.ExecuteAnalysis(@"C:\file\path", analysisId, default, default, default, cancellationTokenSource.Token); - + analysisService.Received().AnalyzeFilesAndTrackAsync(Arg.Any(), cancellationTokenSource.Token); } - + [TestMethod] public void ExecuteAnalysis_AnalysisServiceSucceeds_ExitsWithoutFinishingAnalysis() { var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid"), CreateDefaultAnalysisStatusNotifier(out var notifier)); analysisService.AnalyzeFilesAndTrackAsync(default, default).ReturnsForAnyArgs(new AnalyzeFilesResponse(new HashSet(), [])); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); - + notifier.DidNotReceiveWithAnyArgs().AnalysisNotReady(default); notifier.DidNotReceiveWithAnyArgs().AnalysisFailed(default(Exception)); notifier.DidNotReceiveWithAnyArgs().AnalysisFailed(default(string)); notifier.DidNotReceiveWithAnyArgs().AnalysisFinished(default, default); } - + [TestMethod] public void ExecuteAnalysis_AnalysisServiceFailsForFile_NotifyFailed() { var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid"), CreateDefaultAnalysisStatusNotifier(out var notifier)); analysisService.AnalyzeFilesAndTrackAsync(default, default).ReturnsForAnyArgs(new AnalyzeFilesResponse(new HashSet{new(@"C:\file\path")}, [])); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); - + notifier.Received().AnalysisFailed(SLCoreStrings.AnalysisFailedReason); } - + [TestMethod] public void ExecuteAnalysis_AnalysisServiceCancelled_NotifyCancel() { var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid"), CreateDefaultAnalysisStatusNotifier(out var notifier)); var operationCanceledException = new OperationCanceledException(); analysisService.AnalyzeFilesAndTrackAsync(default, default).ThrowsAsyncForAnyArgs(operationCanceledException); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); - + notifier.Received().AnalysisCancelled(); } - + [TestMethod] public void ExecuteAnalysis_AnalysisServiceThrows_NotifyFailed() { var testSubject = CreateTestSubject(CreatServiceProvider(out var analysisService), CreateInitializedConfigScope("someconfigscopeid"), CreateDefaultAnalysisStatusNotifier(out var notifier)); var exception = new Exception(); analysisService.AnalyzeFilesAndTrackAsync(default, default).ThrowsAsyncForAnyArgs(exception); - + testSubject.ExecuteAnalysis(@"C:\file\path", Guid.NewGuid(), default, default, default, default); - + notifier.Received().AnalysisFailed(exception); } @@ -255,19 +286,28 @@ private static ICurrentTimeProvider CreatCurrentTimeProvider(DateTimeOffset nowT return currentTimeProvider; } - private static SLCoreAnalyzer CreateTestSubject(ISLCoreServiceProvider slCoreServiceProvider = null, IActiveConfigScopeTracker activeConfigScopeTracker = null, - IAnalysisStatusNotifierFactory analysisStatusNotifierFactory = null, - ICurrentTimeProvider currentTimeProvider = null) + IAnalysisStatusNotifierFactory analysisStatusNotifierFactory = null, + ICurrentTimeProvider currentTimeProvider = null, + ICompilationDatabaseLocator compilationDatabaseLocator = null) { slCoreServiceProvider ??= Substitute.For(); activeConfigScopeTracker ??= Substitute.For(); analysisStatusNotifierFactory ??= Substitute.For(); currentTimeProvider ??= Substitute.For(); + compilationDatabaseLocator ??= Substitute.For(); return new SLCoreAnalyzer(slCoreServiceProvider, activeConfigScopeTracker, - analysisStatusNotifierFactory, - currentTimeProvider); + analysisStatusNotifierFactory, + currentTimeProvider, + compilationDatabaseLocator); + } + + private static ICompilationDatabaseLocator WithCompilationDatabase(string compilationDatabasePath) + { + var compilationDatabaseLocator = Substitute.For(); + compilationDatabaseLocator.Locate().Returns(compilationDatabasePath); + return compilationDatabaseLocator; } } diff --git a/src/SLCore/Analysis/SLCoreAnalyzer.cs b/src/SLCore/Analysis/SLCoreAnalyzer.cs index 57959ea221..d4b1a4f0d1 100644 --- a/src/SLCore/Analysis/SLCoreAnalyzer.cs +++ b/src/SLCore/Analysis/SLCoreAnalyzer.cs @@ -22,6 +22,7 @@ using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core.Analysis; using SonarLint.VisualStudio.Core.ConfigurationScope; +using SonarLint.VisualStudio.Core.CFamily; using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.SLCore.Common.Models; using SonarLint.VisualStudio.SLCore.Core; @@ -37,17 +38,20 @@ public class SLCoreAnalyzer : IAnalyzer private readonly IActiveConfigScopeTracker activeConfigScopeTracker; private readonly IAnalysisStatusNotifierFactory analysisStatusNotifierFactory; private readonly ICurrentTimeProvider currentTimeProvider; + private readonly ICompilationDatabaseLocator compilationDatabaseLocator; [ImportingConstructor] - public SLCoreAnalyzer(ISLCoreServiceProvider serviceProvider, + public SLCoreAnalyzer(ISLCoreServiceProvider serviceProvider, IActiveConfigScopeTracker activeConfigScopeTracker, - IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, - ICurrentTimeProvider currentTimeProvider) + IAnalysisStatusNotifierFactory analysisStatusNotifierFactory, + ICurrentTimeProvider currentTimeProvider, + ICompilationDatabaseLocator compilationDatabaseLocator) { this.serviceProvider = serviceProvider; this.activeConfigScopeTracker = activeConfigScopeTracker; this.analysisStatusNotifierFactory = analysisStatusNotifierFactory; this.currentTimeProvider = currentTimeProvider; + this.compilationDatabaseLocator = compilationDatabaseLocator; } public bool IsAnalysisSupported(IEnumerable languages) @@ -57,32 +61,45 @@ public bool IsAnalysisSupported(IEnumerable languages) public void ExecuteAnalysis(string path, Guid analysisId, IEnumerable detectedLanguages, IIssueConsumer consumer, IAnalyzerOptions analyzerOptions, CancellationToken cancellationToken) - { + { var analysisStatusNotifier = analysisStatusNotifierFactory.Create(nameof(SLCoreAnalyzer), path); analysisStatusNotifier.AnalysisStarted(); - + var configurationScope = activeConfigScopeTracker.Current; if (configurationScope is not { IsReadyForAnalysis: true }) { analysisStatusNotifier.AnalysisNotReady(SLCoreStrings.ConfigScopeNotInitialized); return; } - + if (!serviceProvider.TryGetTransientService(out IAnalysisSLCoreService analysisService)) { analysisStatusNotifier.AnalysisFailed(SLCoreStrings.ServiceProviderNotInitialized); return; } - - ExecuteAnalysisInternalAsync(path, configurationScope.Id, analysisId, analyzerOptions, analysisService, analysisStatusNotifier, cancellationToken).Forget(); + + Dictionary extraProperties = []; + if (detectedLanguages != null && detectedLanguages.Contains(AnalysisLanguage.CFamily)) + { + var compilationDatabasePath = compilationDatabaseLocator.Locate(); + if (compilationDatabasePath == null) + { + analysisStatusNotifier.AnalysisFailed(SLCoreStrings.CompilationDatabaseNotFound); + return; + } + extraProperties["sonar.cfamily.compile-commands"] = compilationDatabasePath; + } + + ExecuteAnalysisInternalAsync(path, configurationScope.Id, analysisId, analyzerOptions, analysisService, analysisStatusNotifier, extraProperties, cancellationToken).Forget(); } private async Task ExecuteAnalysisInternalAsync(string path, string configScopeId, - Guid analysisId, + Guid analysisId, IAnalyzerOptions analyzerOptions, IAnalysisSLCoreService analysisService, IAnalysisStatusNotifier analysisStatusNotifier, + Dictionary extraProperties, CancellationToken cancellationToken) { try @@ -92,7 +109,7 @@ private async Task ExecuteAnalysisInternalAsync(string path, configScopeId, analysisId, [new FileUri(path)], - [], + extraProperties, analyzerOptions?.IsOnOpen ?? false, currentTimeProvider.Now.ToUnixTimeMilliseconds()), cancellationToken); diff --git a/src/SLCore/SLCoreStrings.Designer.cs b/src/SLCore/SLCoreStrings.Designer.cs index a8f772b270..be328993b1 100644 --- a/src/SLCore/SLCoreStrings.Designer.cs +++ b/src/SLCore/SLCoreStrings.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -78,6 +77,15 @@ public static string AnalysisReadinessUpdate { } } + /// + /// Looks up a localized string similar to Failed to analyze: compilation database not found. + /// + public static string CompilationDatabaseNotFound { + get { + return ResourceManager.GetString("CompilationDatabaseNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Configuration scope conflict. /// diff --git a/src/SLCore/SLCoreStrings.resx b/src/SLCore/SLCoreStrings.resx index 68a4c04346..b2f9c14853 100644 --- a/src/SLCore/SLCoreStrings.resx +++ b/src/SLCore/SLCoreStrings.resx @@ -156,6 +156,9 @@ Internal analysis failure. See logs above. + + Failed to analyze: compilation database not found + Configuration scope not initialized