diff --git a/src/CFamily.UnitTests/Analysis/CFamily_CallAnalyzerTests.cs b/src/CFamily.UnitTests/Analysis/CFamily_CallAnalyzerTests.cs deleted file mode 100644 index bd45b8f77f..0000000000 --- a/src/CFamily.UnitTests/Analysis/CFamily_CallAnalyzerTests.cs +++ /dev/null @@ -1,319 +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.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.CFamily.SubProcess; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.CFamily.Analysis.UnitTests -{ - [TestClass] - public class CFamily_CLangAnalyzerTests - { - [TestMethod] - public void CallAnalyzer_Succeeds_ReturnsMessages() - { - // Arrange - var dummyProcessRunner = new DummyProcessRunner(MockResponse()); - - // Act - var response = GetResponse(dummyProcessRunner, CreateRequest(), new TestLogger()); - - // Assert - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - - response.Count.Should().Be(1); - response[0].Filename.Should().Be("file.cpp"); - } - - [TestMethod] - public void CallAnalyzer_Fails_ReturnsZeroMessages() - { - // Arrange - var dummyProcessRunner = new DummyProcessRunner(MockEmptyResponse()); - - // Act - var response = GetResponse(dummyProcessRunner, CreateRequest(), new TestLogger()); - - // Assert - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - - response.Should().BeEmpty(); - } - - [TestMethod] - public void CallAnalyzer_RequestWithEnvironmentVariables_EnvVarsArePassed() - { - // Arrange - var envVars = new Dictionary { { "aaa", "bbb" } }; - - var request = CreateRequestMock(); - request.SetupGet(x => x.EnvironmentVariables).Returns(envVars); - - var dummyProcessRunner = new DummyProcessRunner(MockResponse()); - var result = GetResponse(dummyProcessRunner, request.Object, new TestLogger()); - - // Act and Assert - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - dummyProcessRunner.SuppliedProcessRunnerArguments.EnvironmentVariables.Should().BeSameAs(envVars); - } - - [TestMethod] - public void CallAnalyzer_RequestWithReproducer_ReturnsZeroMessages() - { - // Arrange - var request = CreateRequest(new CFamilyAnalyzerOptions { CreateReproducer = true }); - var dummyProcessRunner = new DummyProcessRunner(MockBadEndResponse()); - var result = GetResponse(dummyProcessRunner, request, new TestLogger()); - - // Act and Assert - result.Should().BeEmpty(); - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - } - - [TestMethod] - public void CallAnalyzer_RequestWithReproducer_DiagnosticsFileIsSaved() - { - // Arrange - var logger = new TestLogger(); - var fileSystem = CreateInitializedFileSystem(); - var requestMock = CreateRequestMock(new CFamilyAnalyzerOptions { CreateReproducer = true }); - var dummyProcessRunner = new DummyProcessRunner(MockResponse()); - GetResponse(dummyProcessRunner, requestMock.Object, logger, fileSystem); - - // Act and Assert - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - - fileSystem.AllFiles.Should().BeEquivalentTo(SubProcessFilePaths.RequestConfigFilePath); - requestMock.Verify(x => x.WriteRequestDiagnostics(It.IsAny()), Times.Once); - - logger.AssertPartialOutputStringExists(SubProcessFilePaths.RequestConfigFilePath); - logger.AssertPartialOutputStringExists(SubProcessFilePaths.ReproducerFilePath); - } - - [TestMethod] - public void CallAnalyzer_RequestWithoutReproducer_DiagnosticsFileIsNotSaved() - { - // Arrange - var logger = new TestLogger(); - var fileSystem = CreateInitializedFileSystem(); - var requestMock = CreateRequestMock(new CFamilyAnalyzerOptions { CreateReproducer = false }); - var dummyProcessRunner = new DummyProcessRunner(MockResponse()); - GetResponse(dummyProcessRunner, requestMock.Object, logger, fileSystem); - - // Act and Assert - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - - fileSystem.AllFiles.Should().BeEmpty(); - requestMock.Verify(x => x.WriteRequestDiagnostics(It.IsAny()), Times.Never); - - logger.AssertPartialOutputStringDoesNotExist(SubProcessFilePaths.RequestConfigFilePath); - logger.AssertPartialOutputStringDoesNotExist(SubProcessFilePaths.ReproducerFilePath); - } - - [TestMethod] - public void CallAnalyzer_BadResponse_ThrowsException() - { - // Arrange - var logger = new TestLogger(); - var dummyProcessRunner = new DummyProcessRunner(MockBadEndResponse()); - - Action act = () => GetResponse(dummyProcessRunner, CreateRequest(), logger); - - // Act and Assert - act.Should().Throw().And.Message.Should().Be("Communication issue with the C/C++ analyzer"); - dummyProcessRunner.ExecuteCalled.Should().BeTrue(); - } - - private static IRequest CreateRequest(CFamilyAnalyzerOptions analyzerOptions = null) => - CreateRequestMock(analyzerOptions).Object; - - private static Mock CreateRequestMock(CFamilyAnalyzerOptions analyzerOptions = null) - { - var context = new RequestContext(null, null, null, null, analyzerOptions, false); - var request = new Mock(); - request.Setup(x => x.Context).Returns(context); - return request; - } - - private static MockFileSystem CreateInitializedFileSystem() - { - var fileSystem = new MockFileSystem(); - - // Make sure the expected working directory exists - fileSystem.Directory.CreateDirectory(SubProcessFilePaths.WorkingDirectory); - return fileSystem; - } - - private static List GetResponse(DummyProcessRunner dummyProcessRunner, IRequest request, ILogger logger, - IFileSystem fileSystem = null) - { - return GetResponse(dummyProcessRunner, request, logger, CancellationToken.None, fileSystem ?? new FileSystem()); - } - - private static List GetResponse(DummyProcessRunner dummyProcessRunner, IRequest request, ILogger logger, CancellationToken cancellationToken, - IFileSystem fileSystem) - { - var messages = new List(); - - CLangAnalyzer.ExecuteSubProcess(messages.Add, request, dummyProcessRunner, logger, cancellationToken, fileSystem); - - return messages; - } - - private class DummyProcessRunner : IProcessRunner - { - private readonly byte[] responseToReturn; - - public DummyProcessRunner(byte[] responseToReturn) - { - this.responseToReturn = responseToReturn; - } - - public bool ExecuteCalled { get; private set; } - - public ProcessRunnerArguments SuppliedProcessRunnerArguments { get; private set; } - - public void Execute(ProcessRunnerArguments runnerArgs) - { - ExecuteCalled = true; - - runnerArgs.Should().NotBeNull(); - SuppliedProcessRunnerArguments = runnerArgs; - - // Expecting a single file name as input - runnerArgs.CmdLineArgs.Count().Should().Be(1); - - using (var stream = new MemoryStream(responseToReturn)) - using (var streamReader = new StreamReader(stream)) - { - runnerArgs.HandleOutputStream(streamReader); - } - } - } - - private byte[] MockEmptyResponse() - { - using (MemoryStream stream = new MemoryStream()) - { - BinaryWriter writer = new BinaryWriter(stream); - Protocol.WriteUTF(writer, "OUT"); - - // 0 issues - - // 0 measures - Protocol.WriteUTF(writer, "measures"); - Protocol.WriteInt(writer, 0); - - // 0 symbols - Protocol.WriteUTF(writer, "symbols"); - Protocol.WriteInt(writer, 0); - - Protocol.WriteUTF(writer, "END"); - return stream.ToArray(); - } - } - - private byte[] MockResponse() - { - using (MemoryStream stream = new MemoryStream()) - { - BinaryWriter writer = new BinaryWriter(stream); - Protocol.WriteUTF(writer, "OUT"); - - // 1 issue - Protocol.WriteUTF(writer, "message"); - - Protocol.WriteUTF(writer, "ruleKey"); - Protocol.WriteUTF(writer, "file.cpp"); - Protocol.WriteInt(writer, 10); - Protocol.WriteInt(writer, 11); - Protocol.WriteInt(writer, 12); - Protocol.WriteInt(writer, 13); - Protocol.WriteInt(writer, 100); - Protocol.WriteUTF(writer, "Issue message"); - writer.Write(true); - - // 1 flow - Protocol.WriteInt(writer, 1); - Protocol.WriteUTF(writer, "another.cpp"); - Protocol.WriteInt(writer, 14); - Protocol.WriteInt(writer, 15); - Protocol.WriteInt(writer, 16); - Protocol.WriteInt(writer, 17); - Protocol.WriteUTF(writer, "Flow message"); - - // 0 Data Flow - Protocol.WriteInt(writer, 0); - - // 0 fixes - writer.Write(false); - Protocol.WriteInt(writer, 0); - - // 1 measure - Protocol.WriteUTF(writer, "measures"); - Protocol.WriteInt(writer, 1); - Protocol.WriteUTF(writer, "file.cpp"); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - - byte[] execLines = new byte[] { 1, 2, 3, 4 }; - Protocol.WriteInt(writer, execLines.Length); - writer.Write(execLines); - - // 1 symbol - Protocol.WriteUTF(writer, "symbols"); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - Protocol.WriteInt(writer, 1); - - Protocol.WriteUTF(writer, "END"); - return stream.ToArray(); - } - } - - private byte[] MockBadEndResponse() - { - using (MemoryStream stream = new MemoryStream()) - { - BinaryWriter writer = new BinaryWriter(stream); - Protocol.WriteUTF(writer, "OUT"); - Protocol.WriteUTF(writer, "FOO"); - return stream.ToArray(); - } - } - } -} 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.UnitTests/Analysis/RequestFactoryAggregateTests.cs b/src/CFamily.UnitTests/Analysis/RequestFactoryAggregateTests.cs deleted file mode 100644 index fd084a10d4..0000000000 --- a/src/CFamily.UnitTests/Analysis/RequestFactoryAggregateTests.cs +++ /dev/null @@ -1,116 +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.ComponentModel.Composition; -using System.ComponentModel.Composition.Hosting; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.CFamily.Analysis.UnitTests -{ - [TestClass] - public class RequestFactoryAggregateTests - { - [TestMethod] - public void MefCtor_CheckExports() - { - var batch = new CompositionBatch(); - - batch.AddExport(MefTestHelpers.CreateExport(Mock.Of())); - batch.AddExport(MefTestHelpers.CreateExport(Mock.Of())); - batch.AddExport(MefTestHelpers.CreateExport(Mock.Of())); - - var aggregateImport = new SingleObjectImporter(); - batch.AddPart(aggregateImport); - - using var catalog = new TypeCatalog(typeof(RequestFactoryAggregate)); - using var container = new CompositionContainer(catalog); - container.Compose(batch); - - aggregateImport.Import.Should().NotBeNull(); - } - - [TestMethod] - public void TryGet_NullFilePath_ArgumentNullException() - { - var testSubject = CreateTestSubject(); - - Func act = () => testSubject.TryCreateAsync(null, new CFamilyAnalyzerOptions()); - - act.Should().ThrowExactly().And.ParamName.Should().Be("analyzedFilePath"); - } - - [TestMethod] - public async Task TryGet_NoFactories_Null() - { - var testSubject = CreateTestSubject(); - - var result = await testSubject.TryCreateAsync("path", new CFamilyAnalyzerOptions()); - - result.Should().BeNull(); - } - - [TestMethod] - public async Task TryGet_NoMatchingFactory_Null() - { - var factory1 = new Mock(); - var factory2 = new Mock(); - - var testSubject = CreateTestSubject(factory1.Object, factory2.Object); - - var options = new CFamilyAnalyzerOptions(); - var result = await testSubject.TryCreateAsync("path", options); - - result.Should().BeNull(); - - factory1.Verify(x=> x.TryCreateAsync("path", options), Times.Once); - factory2.Verify(x=> x.TryCreateAsync("path", options), Times.Once); - } - - [TestMethod] - public async Task TryGet_HasMatchingFactory_OtherFactoriesNotChecked() - { - var factory1 = new Mock(); - var factory2 = new Mock(); - var factory3 = new Mock(); - - var requestToReturn = Mock.Of(); - var options = new CFamilyAnalyzerOptions(); - factory2.Setup(x => x.TryCreateAsync("path", options)).Returns(Task.FromResult(requestToReturn)); - - var testSubject = CreateTestSubject(factory1.Object, factory2.Object, factory3.Object); - - var result = await testSubject.TryCreateAsync("path", options); - - result.Should().Be(requestToReturn); - - factory1.Verify(x => x.TryCreateAsync("path", options), Times.Once); - factory2.Verify(x => x.TryCreateAsync("path", options), Times.Once); - factory3.Invocations.Count.Should().Be(0); - } - - private RequestFactoryAggregate CreateTestSubject(params IRequestFactory[] requestFactories) => - new RequestFactoryAggregate(requestFactories); - } -} diff --git a/src/CFamily.UnitTests/IntegrationTests/CFamily_CLangAnalyzer_IntegrationTests.cs b/src/CFamily.UnitTests/IntegrationTests/CFamily_CLangAnalyzer_IntegrationTests.cs deleted file mode 100644 index be98c6589e..0000000000 --- a/src/CFamily.UnitTests/IntegrationTests/CFamily_CLangAnalyzer_IntegrationTests.cs +++ /dev/null @@ -1,157 +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.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using SonarLint.VisualStudio.CFamily.Analysis; -using SonarLint.VisualStudio.CFamily.CMake; -using SonarLint.VisualStudio.CFamily.CompilationDatabase; -using SonarLint.VisualStudio.CFamily.Helpers.UnitTests; -using SonarLint.VisualStudio.CFamily.Rules; -using SonarLint.VisualStudio.CFamily.SubProcess; -using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.TestInfrastructure; - -namespace SonarLint.VisualStudio.CFamily.IntegrationTests -{ - [TestClass] - public class CFamily_CLangAnalyzer_IntegrationTests - { - private string testsDataDirectory; - private string clExe; - - [TestInitialize] - public void TestInitialize() - { - // Uri absolute path is used to make issues filename slashes consistent between expected and actual - testsDataDirectory = new Uri(Path.Combine( - Path.GetDirectoryName(typeof(CFamily_CLangAnalyzer_IntegrationTests).Assembly.Location), - "IntegrationTests\\")).AbsolutePath; - - // Subprocess.exe requires a valid path to an executable named cl.exe that prints something similar to the real compiler - const string code = "Console.Error.WriteLine(\"Microsoft(R) C / C++ Optimizing Compiler Version 19.32.31114.2 for x64\");"; - clExe = DummyExeHelper.CreateDummyExe(testsDataDirectory, "cl.exe", 0, code); - } - - [TestMethod] - [DataRow("CLangAnalyzerTestFile_NoIssues_EmptyFile")] - [DataRow("CLangAnalyzerTestFile_OneIssue")] - [DataRow("CLangAnalyzerTestFile_TwoIssues")] - [DataRow("CLangAnalyzerTestFile_OneIssue_HasSecondaryLocations")] - public void CallAnalyzer_IntegrationTest(string testCaseFileName) - { - var testedFile = Path.Combine(testsDataDirectory, testCaseFileName + ".txt").Replace('/', '\\'); - - // Sanity checks to help with debugging on the CI machine - CheckFileExists(testedFile); - CheckRulesMetadataFilesExist(); - - var request = GetRequest(testedFile); - var expectedMessages = GetExpectedMessages(testCaseFileName, testedFile); - - var messages = InvokeAnalyzer(request); - - messages.Where(x => !string.IsNullOrEmpty(x.Filename)).Should().BeEquivalentTo(expectedMessages, e => e.WithStrictOrdering()); - } - - private static void CheckRulesMetadataFilesExist() - { - var libDirectory = CFamilyShared.CFamilyFilesDirectory; - Console.WriteLine($"[TEST SETUP] Checking CFamily lib directory exists: {libDirectory}"); - Directory.Exists(libDirectory).Should().BeTrue($"[TEST SETUP ERROR] CFamily lib directory could not be found: {libDirectory}"); - - CheckFileExists(Path.Combine(libDirectory, "Sonar_way_profile.json")); - CheckFileExists(Path.Combine(libDirectory, "RulesList.json")); - } - - private static void CheckFileExists(string fileName) - { - Console.WriteLine($"[TEST SETUP] Checking for required file: {fileName}"); - File.Exists(fileName).Should().BeTrue($"[TEST SETUP ERROR] Could not find required test input file: {fileName}"); - } - - private CompilationDatabaseRequest GetRequest(string testedFile) - { - var command = "\"" + clExe + "\" /TP " + testedFile; - var compilationDatabaseEntry = new CompilationDatabaseEntry - { - Directory = testsDataDirectory, - Command = command, - File = testedFile - }; - - var envVars = new ReadOnlyDictionary(new Dictionary { - { "INCLUDE", "" } - }); - - var languageKey = SonarLanguageKeys.CPlusPlus; - - var config = new CFamilySonarWayRulesConfigProvider(CFamilyShared.CFamilyFilesDirectory).GetRulesConfiguration("cpp"); - var context = new RequestContext( - languageKey, - config, - testedFile, - "", - new CFamilyAnalyzerOptions(), - false); - - var request = new CompilationDatabaseRequest(compilationDatabaseEntry, context, envVars); - - return request; - } - - private Message[] GetExpectedMessages(string testFileName, string testedFile) - { - var expectedResponseJson = File.ReadAllText(Path.Combine(testsDataDirectory, testFileName + "_response.json")); - var expectedResponse = JsonConvert.DeserializeObject(expectedResponseJson); - var messages = expectedResponse.Messages; - foreach (var expectedResponseMessage in messages) - { - expectedResponseMessage.Filename = testedFile; - - foreach (var messagePart in expectedResponseMessage.Parts) - { - messagePart.Filename = testedFile; - } - } - - return messages; - } - - private static List InvokeAnalyzer(CompilationDatabaseRequest request) - { - var testLogger = new TestLogger(true); - var processRunner = new ProcessRunner(new ConfigurableSonarLintSettings(), testLogger); - - var messages = new List(); - CLangAnalyzer.ExecuteSubProcess(messages.Add, request, processRunner, testLogger, CancellationToken.None, new FileSystem()); - messages = messages.Where(m => !m.RuleKey.StartsWith("internal.")).ToList(); - return messages; - } - } -} diff --git a/src/CFamily/Analysis/CLangAnalyzer.cs b/src/CFamily/Analysis/CLangAnalyzer.cs index 2cd7d3e9a2..3b3f5dee1b 100644 --- a/src/CFamily/Analysis/CLangAnalyzer.cs +++ b/src/CFamily/Analysis/CLangAnalyzer.cs @@ -19,16 +19,8 @@ */ 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 System.Diagnostics.CodeAnalysis; 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 +34,14 @@ void ExecuteAnalysis(string path, CancellationToken cancellationToken); } - [Export(typeof(IAnalyzer))] + [ExcludeFromCodeCoverage] [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 +49,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.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt index 861cd122d4..ae4dde4b0d 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2024-11-25T15:46:59.9789841Z +# Report date/time: 2024-11-27T13:19:57.1708469Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -70,7 +70,6 @@ Referenced assemblies: - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.8.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' @@ -79,7 +78,7 @@ Referenced assemblies: - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 12 +# Number of references: 11 --- Assembly: 'SonarLint.VisualStudio.ConnectedMode, Version=8.8.0.0, Culture=neutral, PublicKeyToken=c5b62af9de6d7244' diff --git a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt index a34eddab0c..7fbfe23e2c 100644 --- a/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt +++ b/src/Integration.Vsix/AsmRef_Integration.Vsix_Baseline_WithoutStrongNames.txt @@ -1,7 +1,7 @@ --- ################################ # Assembly references report -# Report date/time: 2024-11-25T15:46:59.9789841Z +# Report date/time: 2024-11-27T13:19:57.1708469Z ################################ # # Generated by Devtility CheckAsmRefs v0.11.0.223 @@ -70,7 +70,6 @@ Referenced assemblies: - 'Microsoft.VisualStudio.CoreUtility, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Shell.Framework, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'Microsoft.VisualStudio.Text.Data, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' -- 'Microsoft.VisualStudio.Threading, Version=17.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' - 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' - 'SonarLint.VisualStudio.Core, Version=8.8.0.0, Culture=neutral, PublicKeyToken=null' @@ -79,7 +78,7 @@ Referenced assemblies: - 'System.ComponentModel.Composition, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' - 'System.IO.Abstractions, Version=9.0.0.0, Culture=neutral, PublicKeyToken=96bf224d23c43e59' -# Number of references: 12 +# Number of references: 11 --- Assembly: 'SonarLint.VisualStudio.ConnectedMode, Version=8.8.0.0, Culture=neutral, PublicKeyToken=null' 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..0060faa55b 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,43 @@ 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_DoesNotPassExtraProperty() + { + var compilationDatabaseLocator = WithCompilationDatabase(null); + 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.ContainsKey("sonar.cfamily.compile-commands")), + Arg.Any()); + } + [TestMethod] public void ExecuteAnalysis_PassesCorrectCancellationTokenToAnalysisService() { @@ -164,57 +197,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 +288,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..3eeb323634 100644 --- a/src/SLCore/Analysis/SLCoreAnalyzer.cs +++ b/src/SLCore/Analysis/SLCoreAnalyzer.cs @@ -21,6 +21,7 @@ using System.ComponentModel.Composition; using Microsoft.VisualStudio.Threading; using SonarLint.VisualStudio.Core.Analysis; +using SonarLint.VisualStudio.Core.CFamily; using SonarLint.VisualStudio.Core.ConfigurationScope; using SonarLint.VisualStudio.Core.SystemAbstractions; using SonarLint.VisualStudio.SLCore.Common.Models; @@ -33,21 +34,26 @@ namespace SonarLint.VisualStudio.SLCore.Analysis; [PartCreationPolicy(CreationPolicy.Shared)] public class SLCoreAnalyzer : IAnalyzer { + private const string CFamilyCompileCommandsProperty = "sonar.cfamily.compile-commands"; + private readonly ISLCoreServiceProvider serviceProvider; 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 +63,35 @@ 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(); + + var extraProperties = GetExtraProperties(detectedLanguages); + + 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 +101,7 @@ private async Task ExecuteAnalysisInternalAsync(string path, configScopeId, analysisId, [new FileUri(path)], - [], + extraProperties, analyzerOptions?.IsOnOpen ?? false, currentTimeProvider.Now.ToUnixTimeMilliseconds()), cancellationToken); @@ -112,4 +121,22 @@ [new FileUri(path)], analysisStatusNotifier.AnalysisFailed(e); } } + + private Dictionary GetExtraProperties(IEnumerable detectedLanguages) + { + Dictionary extraProperties = []; + if (!IsCFamily(detectedLanguages)) + { + return extraProperties; + } + + var compilationDatabasePath = compilationDatabaseLocator.Locate(); + if (compilationDatabasePath != null) + { + extraProperties[CFamilyCompileCommandsProperty] = compilationDatabasePath; + } + return extraProperties; + } + + private static bool IsCFamily(IEnumerable detectedLanguages) => detectedLanguages != null && detectedLanguages.Contains(AnalysisLanguage.CFamily); } diff --git a/src/SLCore/SLCoreStrings.Designer.cs b/src/SLCore/SLCoreStrings.Designer.cs index a8f772b270..65439db2e6 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.