From aeb589077846256e5e6d5ba3fadc59be7631d224 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 7 Mar 2024 22:16:19 -0500 Subject: [PATCH 1/4] Analyzer/fix that checks NoWarn appends --- .../CodeFixes/AppendNoWarnFixProvider.cs | 43 ++++++++++ .../Analyzers/AppendNoWarnAnalyzerTest.cs | 38 +++++++++ .../Analyzers/AppendNoWarnAnalyzer.cs | 78 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 MonoDevelop.MSBuild.Editor/CodeFixes/AppendNoWarnFixProvider.cs create mode 100644 MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs create mode 100644 MonoDevelop.MSBuild/Analyzers/AppendNoWarnAnalyzer.cs diff --git a/MonoDevelop.MSBuild.Editor/CodeFixes/AppendNoWarnFixProvider.cs b/MonoDevelop.MSBuild.Editor/CodeFixes/AppendNoWarnFixProvider.cs new file mode 100644 index 00000000..234204d8 --- /dev/null +++ b/MonoDevelop.MSBuild.Editor/CodeFixes/AppendNoWarnFixProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Threading.Tasks; + +using MonoDevelop.MSBuild.Analyzers; +using MonoDevelop.MSBuild.Editor.Analysis; +using MonoDevelop.Xml.Dom; + +namespace MonoDevelop.MSBuild.Editor.CodeFixes +{ + [Export (typeof (MSBuildFixProvider))] + class AppendNoWarnFixProvider : MSBuildFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create ( + AppendNoWarnAnalyzer.DiagnosticId + ); + + public override Task RegisterCodeFixesAsync (MSBuildFixContext context) + { + foreach (var diag in context.Diagnostics) { + if (context.XDocument.FindAtOffset (diag.Span.Start) is XElement prop && prop.InnerSpan is TextSpan valueSpan) { + context.RegisterCodeFix (new PrependListValueAction (valueSpan, "$(NoWarn)"), diag); + } + } + return Task.CompletedTask; + } + + class PrependListValueAction (TextSpan valueSpan, string valueToPrepend) : SimpleMSBuildCodeAction + { + public override string Title => $"Prepend '{valueToPrepend}' to list"; + + protected override MSBuildCodeActionOperation CreateOperation () + { + var op = new EditTextActionOperation (); + op.Insert (valueSpan.Start, valueToPrepend + ";"); + return op; + } + } + } +} diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs b/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs new file mode 100644 index 00000000..70471a71 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Analyzers; + +using NUnit.Framework; + +namespace MonoDevelop.MSBuild.Tests.Analyzers +{ + [TestFixture] + class AppendNoWarnAnalyzerTest : MSBuildAnalyzerTest + { + [TestCase("CS123;CS346;CS567", true)] + [TestCase("CS123", true)] + [TestCase("$(NoWarn)CS234", true)] + [TestCase("$(NoWarn);CS123", false)] + [TestCase("CS563; $(NoWarn) ;CS123", false)] + [TestCase(" $(NoWarn) ", false)] + [TestCase("$(NoWarn);", false)] + public void ValueInSimpleProject (string value, bool isError) + { + var source = $@" + + {value} + +"; + + var analyzer = new AppendNoWarnAnalyzer (); + + MSBuildDiagnostic[] expected = isError + ? [new MSBuildDiagnostic (analyzer.SupportedDiagnostics[0], SpanFromLineColLength (source, 3, 6, 6))] + : []; + + VerifyDiagnostics (source, [ analyzer ], checkUnexpectedDiagnostics: true, expectedDiagnostics: expected); + } + } +} diff --git a/MonoDevelop.MSBuild/Analyzers/AppendNoWarnAnalyzer.cs b/MonoDevelop.MSBuild/Analyzers/AppendNoWarnAnalyzer.cs new file mode 100644 index 00000000..36e76e82 --- /dev/null +++ b/MonoDevelop.MSBuild/Analyzers/AppendNoWarnAnalyzer.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Language.Expressions; +using MonoDevelop.Xml.Dom; + +namespace MonoDevelop.MSBuild.Analyzers +{ + [MSBuildAnalyzer] + class AppendNoWarnAnalyzer : MSBuildAnalyzer + { + public const string DiagnosticId = nameof (AppendNoWarn); + + readonly MSBuildDiagnosticDescriptor AppendNoWarn = new ( + DiagnosticId, + "Append NoWarn values to existing value", + "When settings the `NoWarn` property, you should append the additional values to the existing value of the property. " + + "Otherwise, you may accidentally remove existing values.", + MSBuildDiagnosticSeverity.Warning + ); + + const string NoWarnPropName = "NoWarn"; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create (AppendNoWarn); + + public override void Initialize (MSBuildAnalysisContext context) + { + context.RegisterPropertyWriteAction (AnalyzeProperty, NoWarnPropName); + } + + void AnalyzeProperty (PropertyWriteDiagnosticContext ctx) + { + if (ctx.Node is ListExpression list) { + foreach (var node in list.Nodes) { + if (IsNoWarnPropertyRef (node)) { + return; + } + } + } else if (IsNoWarnPropertyRef (ctx.Node)) { + return; + } + + ctx.ReportDiagnostic (new MSBuildDiagnostic (SupportedDiagnostics[0], ctx.XElement.GetSquiggleSpan ())); + + static bool IsNoWarnPropertyRef (ExpressionNode node) + { + return TryGetTrimmedSingleNode (node) is ExpressionProperty prop && prop.IsSimpleProperty && string.Equals (prop.Name, NoWarnPropName, System.StringComparison.OrdinalIgnoreCase); + } + } + + static ExpressionNode? TryGetTrimmedSingleNode(ExpressionNode node) + { + if (node is not ConcatExpression concat) { + return node; + } + + int start = 0; + if (concat.Nodes[start] is ExpressionText preText && string.IsNullOrWhiteSpace(preText.Value)) { + start++; + } + + int end = concat.Nodes.Count - 1; + if (concat.Nodes[end] is ExpressionText postText && string.IsNullOrWhiteSpace (postText.Value)) { + end--; + } + + int count = start - end + 1; + if (count == 1) { + return concat.Nodes[start]; + } + + return null; + } + } +} \ No newline at end of file From 367a43d88624814f6209e31cfda23c1db25e2b3d Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 9 Mar 2024 01:04:47 -0500 Subject: [PATCH 2/4] Reorganize things to make it easier to test code fixes --- .../Analysis/MSBuildCodeFixService.cs | 9 +- .../Analysis/MSBuildSuggestedActionSource.cs | 2 +- .../MSBuildFindReferencesTests.cs | 5 +- .../MSBuildResolverTests.cs | 6 +- .../MSBuildTestEnvironment.cs | 1 + .../Refactorings/ExtractExpressionTests.cs | 38 +-- .../MSBuildCodeFixTestExtensions.cs | 25 -- .../MSBuildEditorTestExtensions.cs | 221 ++++++++++++++++++ .../MSBuildRefactoringTestExtensions.cs | 62 ----- .../Analyzers/AppendNoWarnAnalyzerTest.cs | 6 +- .../Analyzers/CoreDiagnosticTests.cs | 3 +- ...NotAssignMSBuildAllProjectsAnalyzerTest.cs | 3 +- .../Analyzers/IncompleteDocumentTests.cs | 3 +- .../Analyzers/MSBuildAnalyzerTest.cs | 155 ------------ .../MSBuildExpressionCompletionTest.cs | 3 +- .../MSBuildDocumentTest.Diagnostics.cs | 108 +++++++++ .../Helpers/MSBuildDocumentTest.Parsing.cs | 49 ++++ .../Helpers/MSBuildDocumentTest.Text.cs | 97 ++++++++ .../Helpers/MSBuildDocumentTest.cs | 14 ++ .../Helpers/MSBuildTestHelpers.cs | 96 ++++++++ .../{ => Helpers}/NUnitExtensions.cs | 0 .../TestSchemaProvider.cs | 5 +- .../MSBuildImportEvaluationTests.cs | 3 +- .../MSBuildTestHelpers.cs | 170 -------------- .../Analysis/MSBuildDiagnosticSeverity.cs | 5 +- MonoDevelop.MSBuild/Util/TextWithMarkers.cs | 15 +- 26 files changed, 624 insertions(+), 480 deletions(-) delete mode 100644 MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildCodeFixTestExtensions.cs create mode 100644 MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs delete mode 100644 MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildRefactoringTestExtensions.cs delete mode 100644 MonoDevelop.MSBuild.Tests/Analyzers/MSBuildAnalyzerTest.cs create mode 100644 MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs create mode 100644 MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Parsing.cs create mode 100644 MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs create mode 100644 MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.cs create mode 100644 MonoDevelop.MSBuild.Tests/Helpers/MSBuildTestHelpers.cs rename MonoDevelop.MSBuild.Tests/{ => Helpers}/NUnitExtensions.cs (100%) rename MonoDevelop.MSBuild.Tests/{Analyzers => Helpers}/TestSchemaProvider.cs (89%) delete mode 100644 MonoDevelop.MSBuild.Tests/MSBuildTestHelpers.cs diff --git a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeFixService.cs b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeFixService.cs index 5b08cffa..ee4ef04c 100644 --- a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeFixService.cs +++ b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildCodeFixService.cs @@ -13,6 +13,7 @@ using MonoDevelop.MSBuild.Analysis; using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Language; namespace MonoDevelop.MSBuild.Editor.Analysis { @@ -121,11 +122,11 @@ void ReportFix (MSBuildCodeAction a, ImmutableArray diags) { /// there is a better concept of a durable context/scope to which information can be bound. /// public async Task> GetFixes ( - ITextBuffer buffer, MSBuildParseResult result, SnapshotSpan range, + ITextBuffer buffer, MSBuildRootDocument parsedDocument, IList diagnostics, SnapshotSpan range, MSBuildDiagnosticSeverity requestedSeverities, CancellationToken cancellationToken) { var filteredDiags = ImmutableArray.CreateRange ( - result.Diagnostics.Where (d => range.IntersectsWith (new SnapshotSpan (range.Snapshot, d.Span.Start, d.Span.Length)))); + diagnostics.Where (d => range.IntersectsWith (new SnapshotSpan (range.Snapshot, d.Span.Start, d.Span.Length)))); var fixes = new List (); void ReportFix (MSBuildCodeAction a, ImmutableArray d) @@ -147,8 +148,8 @@ void ReportFix (MSBuildCodeAction a, ImmutableArray d) if (diagnosticIdToFixProviderMap.TryGetValue (diagnostic.Descriptor.Id, out var fixProvider)) { var ctx = new MSBuildFixContext ( buffer, - result.MSBuildDocument, - result.MSBuildDocument.XDocument, + parsedDocument, + parsedDocument.XDocument, new Xml.Dom.TextSpan (range.Start, range.Length), ImmutableArray.Create (diagnostic), ReportFix, cancellationToken); diff --git a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSuggestedActionSource.cs b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSuggestedActionSource.cs index 762a8744..56593eea 100644 --- a/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSuggestedActionSource.cs +++ b/MonoDevelop.MSBuild.Editor/Analysis/MSBuildSuggestedActionSource.cs @@ -74,7 +74,7 @@ async Task> GetSuggestedActionsAsync (ISuggestedActionCateg var severities = CategoriesToSeverity (requestedActionCategories); if (severities != 0) { - actions = await provider.CodeFixService.GetFixes (textBuffer, result, range, severities, cancellationToken); + actions = await provider.CodeFixService.GetFixes (textBuffer, result.MSBuildDocument, result.Diagnostics, range, severities, cancellationToken); for (int i = 0; i < actions.Count; i++) { if (!requestedActionCategories.Contains (actions[i].Category)) { actions.RemoveAt (i); diff --git a/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs b/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs index a501d9bd..d993a7d2 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/MSBuildFindReferencesTests.cs @@ -8,7 +8,6 @@ using MonoDevelop.MSBuild.Editor.Roslyn; using MonoDevelop.MSBuild.Language; -using MonoDevelop.MSBuild.Tests.Editor.Mocks; using MonoDevelop.Xml.Tests; using MonoDevelop.Xml.Parser; @@ -17,7 +16,7 @@ namespace MonoDevelop.MSBuild.Tests { [TestFixture] - public class MSBuildFindReferencesTests + class MSBuildFindReferencesTests : MSBuildDocumentTest { List<(int Offset, int Length, ReferenceUsage Usage)> FindReferences (string docString, MSBuildReferenceKind kind, object reference, [CallerMemberName] string testMethodName = null) { @@ -27,7 +26,7 @@ public class MSBuildFindReferencesTests var (xdoc, _) = xmlParser.Parse (new StringReader (docString)); var logger = TestLoggerFactory.CreateLogger (testMethodName); - var doc = MSBuildTestHelpers.CreateEmptyDocument (); + var doc = CreateEmptyDocument (); var parseContext = new MSBuildParserContext ( new NullMSBuildEnvironment (), null, null, null, "test.csproj", new PropertyValueCollector (false), null, logger, null, default); doc.Build (xdoc, parseContext); diff --git a/MonoDevelop.MSBuild.Tests.Editor/MSBuildResolverTests.cs b/MonoDevelop.MSBuild.Tests.Editor/MSBuildResolverTests.cs index 44aeb713..30d62961 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MSBuildResolverTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/MSBuildResolverTests.cs @@ -34,7 +34,6 @@ using MonoDevelop.MSBuild.Language; using MonoDevelop.MSBuild.Language.Syntax; using MonoDevelop.MSBuild.Language.Typesystem; - using MonoDevelop.Xml.Tests; using NUnit.Framework; @@ -42,13 +41,12 @@ namespace MonoDevelop.MSBuild.Tests { [TestFixture] - public class MSBuildResolverTests + class MSBuildResolverTests : MSBuildDocumentTest { List<(int offset, MSBuildResolveResult result)> Resolve (string doc, ILogger logger) { var functionTypeProvider = new RoslynFunctionTypeProvider (null, logger); - return MSBuildTestHelpers - .SelectAtMarkers (doc, (state) => MSBuildResolver.Resolve (state.parser.Clone (), state.textSource, state.doc, functionTypeProvider, logger)) + return SelectAtMarkers (doc, (state) => MSBuildResolver.Resolve (state.parser.Clone (), state.textSource, state.doc, functionTypeProvider, logger)) .ToList (); } diff --git a/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs b/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs index 129d7cd5..569e5a5f 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/MSBuildTestEnvironment.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using MonoDevelop.MSBuild.Editor.Completion; +using MonoDevelop.MSBuild.Tests.Helpers; using MonoDevelop.Xml.Editor.Tests; namespace MonoDevelop.MSBuild.Tests diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs index ae93a19b..1ca26f85 100644 --- a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs +++ b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/ExtractExpressionTests.cs @@ -5,17 +5,13 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Text.Editor; - using MonoDevelop.MSBuild.Editor.Refactorings.ExtractExpression; using MonoDevelop.MSBuild.Language.Expressions; using MonoDevelop.MSBuild.Language.Syntax; using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Dom; -using MonoDevelop.Xml.Editor.Tests.Extensions; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests.Parser; @@ -87,36 +83,8 @@ public Task ExtractFromTaskToExistingGroupInProject () => TestExtractExpression async Task TestExtractExpression (string textWithMarkers, int expectedFixCount, string invokeFixWithTitle, string expectedTextAfterInvoke, string typeText, string expectedTextAfterTyping) { - var ctx = await this.GetRefactorings (textWithMarkers); - - Assert.That (ctx.CodeFixes, Has.Count.EqualTo (expectedFixCount)); - Assert.That (ctx.CodeFixes.Select (c => c.Action.Title), Has.One.EqualTo (invokeFixWithTitle)); - - var fix = ctx.CodeFixes.Single (c => c.Action.Title == invokeFixWithTitle); - - var operations = await fix.Action.ComputeOperationsAsync (CancellationToken.None); - - var options = ctx.TextView.Options; - options.SetOptionValue (DefaultOptions.ConvertTabsToSpacesOptionId, true); - options.SetOptionValue (DefaultOptions.IndentSizeOptionId, 2); - options.SetOptionValue (DefaultOptions.TabSizeOptionId, 2); - - foreach (var op in operations) { - op.Apply (options, ctx.TextBuffer, CancellationToken.None, ctx.TextView); - } - - Assert.That ( - ctx.TextBuffer.CurrentSnapshot.GetText (), - Is.EqualTo (expectedTextAfterInvoke)); - - // type a new name for the extracted property - await Catalog.JoinableTaskContext.Factory.SwitchToMainThreadAsync (default); - var commandService = Catalog.CommandServiceFactory.GetService (ctx.TextView); - commandService.Type (typeText); - - Assert.That ( - ctx.TextBuffer.CurrentSnapshot.GetText (), - Is.EqualTo (expectedTextAfterTyping)); + var context = await this.GetRefactorings (textWithMarkers); + await this.TestCodeFixContext (context, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping); } [Test] @@ -174,7 +142,7 @@ public void ExtractionPointsFromTarget () ("Target", true), ("Project", true)); } - void CheckExtractionPoints (string textWithMarkers, MSBuildSyntaxKind originKind, params (string scopeName, bool createGroup)[] expectedSpanProps) + static void CheckExtractionPoints (string textWithMarkers, MSBuildSyntaxKind originKind, params (string scopeName, bool createGroup)[] expectedSpanProps) { var doc = TextWithMarkers.Parse (textWithMarkers, '^', '$'); diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildCodeFixTestExtensions.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildCodeFixTestExtensions.cs deleted file mode 100644 index cc374fb1..00000000 --- a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildCodeFixTestExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.Text; - -using MonoDevelop.MSBuild.Analysis; -using MonoDevelop.MSBuild.Editor.Analysis; - -namespace MonoDevelop.MSBuild.Tests.Editor.Refactorings -{ - static class MSBuildCodeFixTestExtensions - { - public static async Task> GetFixes (this MSBuildEditorTest test, MSBuildCodeFixService codeFixService, ITextBuffer buffer, SnapshotSpan range, MSBuildDiagnosticSeverity requestedSeverities, CancellationToken cancellationToken = default) - { - var parser = test.GetParser (buffer); - var parseResult = await parser.GetOrProcessAsync (buffer.CurrentSnapshot, cancellationToken); - - return await codeFixService.GetFixes (buffer, parseResult, range, requestedSeverities, cancellationToken); - } - } -} diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs new file mode 100644 index 00000000..be73f914 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildEditorTestExtensions.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Editor.Analysis; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.MSBuild.Util; +using MonoDevelop.Xml.Editor.Tests.Extensions; +using MonoDevelop.Xml.Tests; + +using NUnit.Framework; + +namespace MonoDevelop.MSBuild.Tests.Editor +{ + readonly record struct CodeFixesWithContext (List CodeFixes, ITextBuffer TextBuffer, ITextView TextView); + + static class MSBuildEditorTestExtensions + { + public static async Task GetRefactorings (this MSBuildEditorTest test, MSBuildRefactoringService refactoringService, ITextView textView, CancellationToken cancellationToken = default) + { + // TODO: use a custom parser provider that constrains the analyzers + var buffer = textView.TextBuffer; + var parser = test.GetParser (buffer); + + var selection = textView.Selection.SelectedSpans.Single (); + + var parseResult = await parser.GetOrProcessAsync (buffer.CurrentSnapshot, cancellationToken); + + return new ( + await refactoringService.GetRefactorings (parseResult, selection, cancellationToken), + buffer, + textView + ); + } + + public static Task GetRefactorings (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker = '|', CancellationToken cancellationToken = default) + where T : MSBuildRefactoringProvider, new() + { + var refactoringService = new MSBuildRefactoringService (new[] { new T () }); + var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker); + + return test.GetRefactorings (refactoringService, textView, cancellationToken); + } + + public static ITextView CreateTextViewWithSelection (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker) + { + var parsed = TextWithMarkers.Parse (documentWithSelection, selectionMarker); + var text = parsed.Text; + var selection = parsed.GetMarkedSpan (selectionMarker); + + var textView = test.CreateTextView (text); + + textView.Caret.MoveTo (new SnapshotPoint (textView.TextBuffer.CurrentSnapshot, selection.End)); + + var selectedSpan = new SnapshotSpan (textView.TextBuffer.CurrentSnapshot, selection.Start, selection.Length); + textView.Selection.Select (selectedSpan, false); + + return textView; + } + + public static ITextView CreateTextViewWithCaret (this MSBuildEditorTest test, string documentWithSelection, char caretMarker) + { + var parsed = TextWithMarkers.Parse (documentWithSelection, caretMarker); + var text = parsed.Text; + var position = parsed.GetMarkedPosition (caretMarker); + + var textView = test.CreateTextView (text); + + return textView; + } + + public static async Task TestRefactoring ( + this MSBuildEditorTest test, + string documentWithSelection, + string invokeFixWithTitle, + int expectedFixCount, + string expectedTextAfterInvoke, + string? typeText = null, + string? expectedTextAfterTyping = null, + char selectionMarker = '|', + CancellationToken cancellationToken = default + ) where T : MSBuildRefactoringProvider, new() + { + var ctx = await test.GetRefactorings (documentWithSelection, selectionMarker, cancellationToken); + await test.TestCodeFixContext(ctx, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping, cancellationToken); + } + + // TODO: allow caller to provide a more limited set of analyzers to run + public static async Task GetCodeFixes ( + this MSBuildEditorTest test, + ICollection analyzers, + ICollection codeFixes, + ITextView textView, + SnapshotSpan range, + MSBuildDiagnosticSeverity + requestedSeverities, + bool includeCoreDiagnostics = false, + MSBuildSchema? schema = null, + ILogger? logger = null, + CancellationToken cancellationToken = default) + { + logger ??= TestLoggerFactory.CreateTestMethodLogger ().RethrowExceptions (); + + var snapshot = textView.TextBuffer.CurrentSnapshot; + var diagnostics = MSBuildDocumentTest.GetDiagnostics (snapshot.GetText (), out var parsedDocument, analyzers, includeCoreDiagnostics, logger, schema, null, cancellationToken); + + var codeFixService = new MSBuildCodeFixService (codeFixes.ToArray ()); + var fixes = await codeFixService.GetFixes (textView.TextBuffer, parsedDocument, diagnostics, range, requestedSeverities, cancellationToken); + + return new CodeFixesWithContext (fixes, textView.TextBuffer, textView); + } + + public static Task GetCodeFixes ( + this MSBuildEditorTest test, + ITextView textView, + SnapshotSpan range, + MSBuildDiagnosticSeverity requestedSeverities, + ILogger? logger = null, + CancellationToken cancellationToken = default + ) + where TAnalyzer : MSBuildAnalyzer, new() + where TCodeFix : MSBuildFixProvider, new() + { + return test.GetCodeFixes ([new TAnalyzer ()], [new TCodeFix ()], textView, range, requestedSeverities, false, null, logger, cancellationToken); + } + public static Task GetCodeFixes ( + this MSBuildEditorTest test, + string documentWithSelection, + MSBuildDiagnosticSeverity requestedSeverities = MSBuildDiagnosticSeverity.All, + char selectionMarker = '|', + ILogger? logger = null, + CancellationToken cancellationToken = default + ) + where TAnalyzer : MSBuildAnalyzer, new() + where TCodeFix : MSBuildFixProvider, new() + { + var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker); + + return test.GetCodeFixes ([new TAnalyzer ()], [new TCodeFix ()], textView, textView.Selection.SelectedSpans.Single(), requestedSeverities, false, null, logger, cancellationToken); + } + + public static async Task TestCodeFix ( + this MSBuildEditorTest test, + string documentWithSelection, + string invokeFixWithTitle, + int expectedFixCount, + string expectedTextAfterInvoke, + string? typeText = null, + string? expectedTextAfterTyping = null, + char selectionMarker = '|', + CancellationToken cancellationToken = default + ) + where TAnalyzer : MSBuildAnalyzer, new() + where TCodeFix : MSBuildFixProvider, new() + { + var ctx = await test.GetCodeFixes (documentWithSelection, selectionMarker: selectionMarker, cancellationToken: cancellationToken); + await test.TestCodeFixContext (ctx, invokeFixWithTitle, expectedFixCount, expectedTextAfterInvoke, typeText, expectedTextAfterTyping, cancellationToken); + } + + public static async Task TestCodeFixContext ( + this MSBuildEditorTest test, + CodeFixesWithContext ctx, + string invokeFixWithTitle, + int expectedFixCount, + string expectedTextAfterInvoke, + string? typeText = null, + string? expectedTextAfterTyping = null, + CancellationToken cancellationToken = default + ) + { + Assert.That (ctx.CodeFixes, Has.Count.EqualTo (expectedFixCount)); + Assert.That (ctx.CodeFixes.Select (c => c.Action.Title), Has.One.EqualTo (invokeFixWithTitle)); + + var fix = ctx.CodeFixes.Single (c => c.Action.Title == invokeFixWithTitle); + + var operations = await fix.Action.ComputeOperationsAsync (cancellationToken); + + var options = ctx.TextView.Options; + options.SetOptionValue (DefaultOptions.ConvertTabsToSpacesOptionId, true); + options.SetOptionValue (DefaultOptions.IndentSizeOptionId, 2); + options.SetOptionValue (DefaultOptions.TabSizeOptionId, 2); + + foreach (var op in operations) { + op.Apply (options, ctx.TextBuffer, CancellationToken.None, ctx.TextView); + } + + Assert.That ( + ctx.TextBuffer.CurrentSnapshot.GetText (), + Is.EqualTo (expectedTextAfterInvoke)); + + if (typeText is null) { + return; + } + + if (expectedTextAfterTyping is null) { + throw new ArgumentNullException (nameof(expectedTextAfterTyping), $"Argument '{expectedTextAfterTyping}' mus not be null when '{typeText}' is not null"); + } + + // the refactoring may have left multiple selections sp the user can e.g. type a new name for an extracted property + await test.Catalog.JoinableTaskContext.Factory.SwitchToMainThreadAsync (default); + var commandService = test.Catalog.CommandServiceFactory.GetService (ctx.TextView); + commandService.Type (typeText); + + Assert.That ( + ctx.TextBuffer.CurrentSnapshot.GetText (), + Is.EqualTo (expectedTextAfterTyping)); + } + } +} diff --git a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildRefactoringTestExtensions.cs b/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildRefactoringTestExtensions.cs deleted file mode 100644 index b8988652..00000000 --- a/MonoDevelop.MSBuild.Tests.Editor/Refactorings/MSBuildRefactoringTestExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#nullable enable - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; - -using MonoDevelop.MSBuild.Editor.Analysis; -using MonoDevelop.MSBuild.Util; - -namespace MonoDevelop.MSBuild.Tests.Editor.Refactorings -{ - readonly record struct CodeFixesWithContext (List CodeFixes, ITextBuffer TextBuffer, ITextView TextView); - - static class MSBuildRefactoringTestExtensions - { - public static async Task GetRefactorings (this MSBuildEditorTest test, MSBuildRefactoringService refactoringService, ITextView textView, CancellationToken cancellationToken = default) - { - // TODO: use a custom parser provider that constrains the analyzers - var buffer = textView.TextBuffer; - var parser = test.GetParser (buffer); - - var selection = textView.Selection.SelectedSpans.Single (); - - var parseResult = await parser.GetOrProcessAsync (buffer.CurrentSnapshot, cancellationToken); - - return new ( - await refactoringService.GetRefactorings (parseResult, selection, cancellationToken), - buffer, - textView - ); - } - - - public static Task GetRefactorings (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker = '|', CancellationToken cancellationToken = default) - where T : MSBuildRefactoringProvider, new() - { - var refactoringService = new MSBuildRefactoringService (new[] { new T () }); - var textView = test.CreateTextViewWithSelection (documentWithSelection, selectionMarker); - - return test.GetRefactorings (refactoringService, textView, cancellationToken); - } - - public static ITextView CreateTextViewWithSelection (this MSBuildEditorTest test, string documentWithSelection, char selectionMarker) - { - var parsed = TextWithMarkers.Parse (documentWithSelection, selectionMarker); - var text = parsed.Text; - var selection = parsed.GetMarkedSpan (selectionMarker); - - var textView = test.CreateTextView (text); - var selectedSpan = new SnapshotSpan (textView.TextBuffer.CurrentSnapshot, selection.Start, selection.Length); - textView.Selection.Select (selectedSpan, false); - return textView; - } - } -} diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs b/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs index 70471a71..ddae8cc0 100644 --- a/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs +++ b/MonoDevelop.MSBuild.Tests/Analyzers/AppendNoWarnAnalyzerTest.cs @@ -9,7 +9,7 @@ namespace MonoDevelop.MSBuild.Tests.Analyzers { [TestFixture] - class AppendNoWarnAnalyzerTest : MSBuildAnalyzerTest + class AppendNoWarnAnalyzerTest : MSBuildDocumentTest { [TestCase("CS123;CS346;CS567", true)] [TestCase("CS123", true)] @@ -29,10 +29,10 @@ public void ValueInSimpleProject (string value, bool isError) var analyzer = new AppendNoWarnAnalyzer (); MSBuildDiagnostic[] expected = isError - ? [new MSBuildDiagnostic (analyzer.SupportedDiagnostics[0], SpanFromLineColLength (source, 3, 6, 6))] + ? [new MSBuildDiagnostic (analyzer.SupportedDiagnostics[0], MSBuildDocumentTest.SpanFromLineColLength (source, 3, 6, 6))] : []; - VerifyDiagnostics (source, [ analyzer ], checkUnexpectedDiagnostics: true, expectedDiagnostics: expected); + MSBuildDocumentTest.VerifyDiagnostics (source, [ analyzer ], checkUnexpectedDiagnostics: true, expectedDiagnostics: expected); } } } diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/CoreDiagnosticTests.cs b/MonoDevelop.MSBuild.Tests/Analyzers/CoreDiagnosticTests.cs index 201471eb..4a4497db 100644 --- a/MonoDevelop.MSBuild.Tests/Analyzers/CoreDiagnosticTests.cs +++ b/MonoDevelop.MSBuild.Tests/Analyzers/CoreDiagnosticTests.cs @@ -14,7 +14,7 @@ namespace MonoDevelop.MSBuild.Tests.Analyzers { [TestFixture] - class CoreDiagnosticTests : MSBuildAnalyzerTest + class CoreDiagnosticTests : MSBuildDocumentTest { [Test] public void NoImports () @@ -28,7 +28,6 @@ public void NoImports () VerifyDiagnostics (source, null, true, expected); } - [Test] public void InvalidBool () { diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/DoNotAssignMSBuildAllProjectsAnalyzerTest.cs b/MonoDevelop.MSBuild.Tests/Analyzers/DoNotAssignMSBuildAllProjectsAnalyzerTest.cs index b34a681b..3f9293f6 100644 --- a/MonoDevelop.MSBuild.Tests/Analyzers/DoNotAssignMSBuildAllProjectsAnalyzerTest.cs +++ b/MonoDevelop.MSBuild.Tests/Analyzers/DoNotAssignMSBuildAllProjectsAnalyzerTest.cs @@ -3,13 +3,12 @@ using MonoDevelop.MSBuild.Analysis; using MonoDevelop.MSBuild.Analyzers; - using NUnit.Framework; namespace MonoDevelop.MSBuild.Tests.Analyzers { [TestFixture] - class DoNotAssignMSBuildAllProjectsAnalyzerTest : MSBuildAnalyzerTest + class DoNotAssignMSBuildAllProjectsAnalyzerTest : MSBuildDocumentTest { [Test] public void SingleAssign () diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/IncompleteDocumentTests.cs b/MonoDevelop.MSBuild.Tests/Analyzers/IncompleteDocumentTests.cs index f0613fb0..ee0f702b 100644 --- a/MonoDevelop.MSBuild.Tests/Analyzers/IncompleteDocumentTests.cs +++ b/MonoDevelop.MSBuild.Tests/Analyzers/IncompleteDocumentTests.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using MonoDevelop.MSBuild.Language; - using NUnit.Framework; namespace MonoDevelop.MSBuild.Tests.Analyzers { [TestFixture] - class IncompleteDocumentTests : MSBuildAnalyzerTest + class IncompleteDocumentTests : MSBuildDocumentTest { [Test] [TestCase ("")] diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/MSBuildAnalyzerTest.cs b/MonoDevelop.MSBuild.Tests/Analyzers/MSBuildAnalyzerTest.cs deleted file mode 100644 index e1659b11..00000000 --- a/MonoDevelop.MSBuild.Tests/Analyzers/MSBuildAnalyzerTest.cs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -using MonoDevelop.MSBuild.Analysis; -using MonoDevelop.MSBuild.Language; -using MonoDevelop.MSBuild.Schema; -using MonoDevelop.Xml.Dom; -using MonoDevelop.Xml.Parser; -using MonoDevelop.Xml.Tests; - -using NUnit.Framework; - -namespace MonoDevelop.MSBuild.Tests.Analyzers -{ - class MSBuildAnalyzerTest - { - [OneTimeSetUp] - public void LoadMSBuild () => MSBuildTestHelpers.RegisterMSBuildAssemblies (); - - protected void VerifyDiagnostics (string source, MSBuildAnalyzer analyzer, params MSBuildDiagnostic[] expectedDiagnostics) - => VerifyDiagnostics (source, out _, new[] { analyzer }, false, false, null, expectedDiagnostics); - - protected void VerifyDiagnostics (string source, ICollection analyzers, bool includeCoreDiagnostics, params MSBuildDiagnostic[] expectedDiagnostics) - => VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, false, null, expectedDiagnostics); - - protected void VerifyDiagnostics ( - string source, - ICollection analyzers = null, - bool includeCoreDiagnostics = false, - bool checkUnexpectedDiagnostics = false, - MSBuildSchema schema = null, - MSBuildDiagnostic[] expectedDiagnostics = null, - MSBuildRootDocument previousDocument = null - ) - => VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, checkUnexpectedDiagnostics, schema, expectedDiagnostics, previousDocument); - - protected void VerifyDiagnostics ( - string source, - out MSBuildRootDocument parsedDocument, - ICollection analyzers = null, - bool includeCoreDiagnostics = false, - bool checkUnexpectedDiagnostics = false, - MSBuildSchema schema = null, - MSBuildDiagnostic[] expectedDiagnostics = null, - MSBuildRootDocument previousDocument = null - ) - { - var actualDiagnostics = GetDiagnostics (source, out parsedDocument, analyzers, includeCoreDiagnostics, schema, previousDocument); - - foreach (var expectedDiag in expectedDiagnostics ?? []) { - bool found = false; - for (int i = 0; i < actualDiagnostics.Count; i++) { - var actualDiag = actualDiagnostics[i]; - if (actualDiag.Descriptor == expectedDiag.Descriptor && actualDiag.Span.Equals (expectedDiag.Span)) { - Assert.That (actualDiag.Properties ?? Enumerable.Empty>(), - Is.EquivalentTo (expectedDiag.Properties ?? Enumerable.Empty> ()) - .UsingDictionaryComparer ()); - found = true; - actualDiagnostics.RemoveAt (i); - break; - } - } - if (!found) { - Assert.Fail ($"Did not find expected diagnostic {expectedDiag.Descriptor.Id}@{expectedDiag.Span.Start}-{expectedDiag.Span.End}"); - } - } - - if (checkUnexpectedDiagnostics && actualDiagnostics.Count > 0) { - Assert.Fail ($"Found unexpected diagnostics: {string.Join ("", actualDiagnostics.Select (diag => $"\n\t{diag.Descriptor.Id}@{diag.Span.Start}-{diag.Span.End}"))}"); - } - } - - protected IList GetDiagnostics ( - string source, - out MSBuildRootDocument parsedDocument, - ICollection analyzers = null, - bool includeCoreDiagnostics = false, - MSBuildSchema schema = null, - MSBuildRootDocument previousDocument = null - ) - { - const string projectFileName = "FakeProject.csproj"; - - var token = CancellationToken.None; - - var schemas = new TestSchemaProvider (); - if (schema is not null) { - schemas.AddTestSchema (projectFileName, null, schema); - } - - var environment = new NullMSBuildEnvironment (); - var taskMetadataBuilder = new NoopTaskMetadataBuilder (); - - // internal errors should cause test failure - var logger = TestLoggerFactory.CreateTestMethodLogger ().RethrowExceptions (); - - parsedDocument = MSBuildRootDocument.Parse ( - new StringTextSource (source), - projectFileName, - previousDocument, - schemas, - environment, - taskMetadataBuilder, - logger, - token); - - var analyzerDriver = new MSBuildAnalyzerDriver (logger); - - if (analyzers != null && analyzers.Count > 0) { - analyzerDriver.AddAnalyzers (analyzers); - } else if (!includeCoreDiagnostics) { - throw new ArgumentException ("Analyzers can only be null or empty if core diagnostics are included", nameof (analyzers)); - } - - var actualDiagnostics = analyzerDriver.Analyze (parsedDocument, includeCoreDiagnostics, token); - - return actualDiagnostics ?? []; - } - - protected TextSpan SpanFromLineColLength (string text, int line, int startCol, int length) - { - int currentLine = 1, currentCol = 1; - for (int offset = 0; offset < text.Length; offset++) { - if (currentLine == line && currentCol == startCol) { - return new TextSpan (offset, length); - } - char c = text[offset]; - switch (c) { - case '\r': - if (offset + 1 < text.Length && text[offset + 1] == '\n') { - offset++; - } - goto case '\n'; - case '\n': - if (currentLine == line) { - throw new ArgumentOutOfRangeException ($"Line {currentLine} ended at col {currentCol}"); - } - currentLine++; - currentCol = 1; - break; - default: - currentCol++; - break; - } - } - throw new ArgumentOutOfRangeException ($"Reached line {currentLine}"); - } - } -} diff --git a/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs b/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs index 09dccd12..0e884eda 100644 --- a/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs +++ b/MonoDevelop.MSBuild.Tests/Completion/MSBuildExpressionCompletionTest.cs @@ -10,6 +10,7 @@ using MonoDevelop.MSBuild.Language.Expressions; using MonoDevelop.MSBuild.Language.Typesystem; using MonoDevelop.MSBuild.Schema; +using MonoDevelop.MSBuild.Tests.Helpers; using MonoDevelop.MSBuild.Util; using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; @@ -36,7 +37,7 @@ protected IEnumerable GetExpressionCompletion ( var token = CancellationToken.None; - var schemas = new Analyzers.TestSchemaProvider (); + var schemas = new TestSchemaProvider (); if (schema is not null) { schemas.AddTestSchema (projectFileName, null, schema); } diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs new file mode 100644 index 00000000..054fe12d --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Diagnostics.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Analysis; +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Schema; + +using MonoDevelop.Xml.Tests; + +using NUnit.Framework; + +namespace MonoDevelop.MSBuild.Tests; + +partial class MSBuildDocumentTest +{ + public static IList GetDiagnostics ( + string source, + out MSBuildRootDocument parsedDocument, + ICollection? analyzers = null, + bool includeCoreDiagnostics = false, + ILogger? logger = null, + MSBuildSchema? schema = null, + MSBuildRootDocument? previousDocument = null, + CancellationToken cancellationToken = default + ) + { + // internal errors should cause test failure + logger ??= TestLoggerFactory.CreateTestMethodLogger ().RethrowExceptions (); + + parsedDocument = GetParsedDocument (source, logger, schema, previousDocument, cancellationToken); + + var analyzerDriver = new MSBuildAnalyzerDriver (logger); + + if (analyzers != null && analyzers.Count > 0) { + analyzerDriver.AddAnalyzers (analyzers); + } else if (!includeCoreDiagnostics) { + throw new ArgumentException ("Analyzers can only be null or empty if core diagnostics are included", nameof (analyzers)); + } + + var actualDiagnostics = analyzerDriver.Analyze (parsedDocument, includeCoreDiagnostics, cancellationToken); + + return actualDiagnostics ?? []; + } + + public static void VerifyDiagnostics (string source, MSBuildAnalyzer analyzer, params MSBuildDiagnostic[] expectedDiagnostics) + => VerifyDiagnostics (source, out _, new[] { analyzer }, false, false, null, expectedDiagnostics); + + public static void VerifyDiagnostics (string source, ICollection analyzers, bool includeCoreDiagnostics, params MSBuildDiagnostic[] expectedDiagnostics) + => VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, false, null, expectedDiagnostics); + + public static void VerifyDiagnostics ( + string source, + ICollection? analyzers = null, + bool includeCoreDiagnostics = false, + bool checkUnexpectedDiagnostics = false, + MSBuildSchema? schema = null, + MSBuildDiagnostic[]? expectedDiagnostics = null, + ILogger? logger = null, + MSBuildRootDocument? previousDocument = null + ) + => VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, checkUnexpectedDiagnostics, schema, expectedDiagnostics, logger, previousDocument); + + public static void VerifyDiagnostics ( + string source, + out MSBuildRootDocument parsedDocument, + ICollection? analyzers = null, + bool includeCoreDiagnostics = false, + bool checkUnexpectedDiagnostics = false, + MSBuildSchema? schema = null, + MSBuildDiagnostic[]? expectedDiagnostics = null, + ILogger? logger = null, + MSBuildRootDocument? previousDocument = null + ) + { + var actualDiagnostics = GetDiagnostics (source, out parsedDocument, analyzers, includeCoreDiagnostics, logger, schema, previousDocument); + + foreach (var expectedDiag in expectedDiagnostics ?? []) { + bool found = false; + for (int i = 0; i < actualDiagnostics.Count; i++) { + var actualDiag = actualDiagnostics[i]; + if (actualDiag.Descriptor == expectedDiag.Descriptor && actualDiag.Span.Equals (expectedDiag.Span)) { + Assert.That (actualDiag.Properties ?? Enumerable.Empty> (), + Is.EquivalentTo (expectedDiag.Properties ?? Enumerable.Empty> ()) + .UsingDictionaryComparer ()); + found = true; + actualDiagnostics.RemoveAt (i); + break; + } + } + if (!found) { + Assert.Fail ($"Did not find expected diagnostic {expectedDiag.Descriptor.Id}@{expectedDiag.Span.Start}-{expectedDiag.Span.End}"); + } + } + + if (checkUnexpectedDiagnostics && actualDiagnostics.Count > 0) { + Assert.Fail ($"Found unexpected diagnostics: {string.Join ("", actualDiagnostics.Select (diag => $"\n\t{diag.Descriptor.Id}@{diag.Span.Start}-{diag.Span.End}"))}"); + } + } +} diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Parsing.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Parsing.cs new file mode 100644 index 00000000..f6f1e9b4 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Parsing.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; + +using Microsoft.Extensions.Logging; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Schema; +using MonoDevelop.MSBuild.Tests.Helpers; +using MonoDevelop.Xml.Parser; +using MonoDevelop.Xml.Tests; + +namespace MonoDevelop.MSBuild.Tests; + +partial class MSBuildDocumentTest +{ + public static MSBuildRootDocument GetParsedDocument ( + string source, + ILogger logger = null, + MSBuildSchema schema = null, + MSBuildRootDocument previousDocument = null, + CancellationToken cancellationToken = default + ) + { + // internal errors should cause test failure + logger ??= TestLoggerFactory.CreateTestMethodLogger ().RethrowExceptions (); + + const string projectFileName = "FakeProject.csproj"; + + var schemas = new TestSchemaProvider (); + if (schema is not null) { + schemas.AddTestSchema (projectFileName, null, schema); + } + + var environment = new NullMSBuildEnvironment (); + var taskMetadataBuilder = new NoopTaskMetadataBuilder (); + + return MSBuildRootDocument.Parse ( + new StringTextSource (source), + projectFileName, + previousDocument, + schemas, + environment, + taskMetadataBuilder, + logger, + cancellationToken); + } +} diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs new file mode 100644 index 00000000..d58d9104 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.Text.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +using MonoDevelop.MSBuild.Language; +using MonoDevelop.MSBuild.Util; + +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Parser; +using MonoDevelop.Xml.Tests; + +namespace MonoDevelop.MSBuild.Tests; + +partial class MSBuildDocumentTest +{ + public const char DefaultSelectionMarker = '|'; + + public static IEnumerable<(int index, T result)> SelectAtMarkers ( + string docString, + Func<(XmlSpineParser parser, ITextSource textSource, MSBuildDocument doc, int offset), T> selector, + string filename = null, + char marker = DefaultSelectionMarker) + => SelectAtMarkers (TextWithMarkers.Parse (docString, marker), selector, filename); + + public static IEnumerable<(int index, T result)> SelectAtMarkers ( + TextWithMarkers text, + Func<(XmlSpineParser parser, ITextSource textSource, MSBuildDocument doc, int offset), T> selector, + string filename = null, + char? marker = null) + { + var indices = new Queue (text.GetMarkedPositions (marker)); + + var textDoc = new StringTextSource (text.Text); + + var treeParser = new XmlTreeParser (new XmlRootState ()); + var (xdoc, _) = treeParser.Parse (textDoc.CreateReader ()); + var logger = TestLoggerFactory.CreateTestMethodLogger (); + var parseContext = new MSBuildParserContext ( + new NullMSBuildEnvironment (), null, null, null, filename ?? "test.csproj", new PropertyValueCollector (false), null, logger, null, default); + var doc = CreateEmptyDocument (); + doc.Build (xdoc, parseContext); + + var parser = new XmlSpineParser (treeParser.RootState); + + var nextIndex = indices.Dequeue (); + for (int i = 0; i < textDoc.Length; i++) { + parser.Push (textDoc[i]); + if (parser.Position != nextIndex) { + continue; + } + + yield return (i, selector ((parser, textDoc, doc, i))); + + if (indices.Count == 0) { + break; + } + nextIndex = indices.Dequeue (); + } + } + + public static TextSpan SpanFromLineColLength (string text, int line, int startCol, int length) + { + int currentLine = 1, currentCol = 1; + for (int offset = 0; offset < text.Length; offset++) { + if (currentLine == line && currentCol == startCol) { + return new TextSpan (offset, length); + } + char c = text[offset]; + switch (c) { + case '\r': + if (offset + 1 < text.Length && text[offset + 1] == '\n') { + offset++; + } + goto case '\n'; + case '\n': + if (currentLine == line) { + throw new ArgumentOutOfRangeException ($"Line {currentLine} ended at col {currentCol}"); + } + currentLine++; + currentCol = 1; + break; + default: + currentCol++; + break; + } + } + throw new ArgumentOutOfRangeException ($"Reached line {currentLine}"); + } + + internal static MSBuildDocument CreateEmptyDocument () + { + return new MSBuildDocument (null, false); + } + +} diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.cs new file mode 100644 index 00000000..da70d4ad --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildDocumentTest.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MonoDevelop.MSBuild.Tests.Helpers; + +using NUnit.Framework; + +namespace MonoDevelop.MSBuild.Tests; + +partial class MSBuildDocumentTest +{ + [OneTimeSetUp] + public void LoadMSBuild () => MSBuildTestHelpers.RegisterMSBuildAssemblies (); +} diff --git a/MonoDevelop.MSBuild.Tests/Helpers/MSBuildTestHelpers.cs b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildTestHelpers.cs new file mode 100644 index 00000000..b422e957 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests/Helpers/MSBuildTestHelpers.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; + +using MonoDevelop.MSBuild.Util; + +namespace MonoDevelop.MSBuild.Tests.Helpers; + +static class MSBuildTestHelpers +{ + static bool registeredAssemblies; + + public static void RegisterMSBuildAssemblies () + { + if (registeredAssemblies) { + return; + } + registeredAssemblies = true; + +#if NETFRAMEWORK + if (Platform.IsWindows) { + var vs17Instance = Microsoft.Build.Locator.MSBuildLocator.QueryVisualStudioInstances () + .FirstOrDefault (x => x.DiscoveryType == Microsoft.Build.Locator.DiscoveryType.VisualStudioSetup && x.Version.Major >= 17); + if (vs17Instance == null) { + throw new InvalidOperationException ("Did not find instance of Visual Studio 17.0 or later"); + } + Microsoft.Build.Locator.MSBuildLocator.RegisterInstance (vs17Instance); + return; + } +#endif + var dotnetInstance = Microsoft.Build.Locator.MSBuildLocator.QueryVisualStudioInstances () + .FirstOrDefault (x => x.DiscoveryType == Microsoft.Build.Locator.DiscoveryType.DotNetSdk && x.Version.Major >= 6.0); + if (dotnetInstance == null) { + throw new InvalidOperationException ("Did not find instance of .NET 6.0 or later"); + } + Microsoft.Build.Locator.MSBuildLocator.RegisterInstance (dotnetInstance); + return; + } + + /* + // might need this again, keep it around for now + void FindMSBuildInPath () + { + var msbuildInPath = FindInPath ("msbuild"); + if (msbuildInPath != null) { + //attempt to read the msbuild.dll location from the launch script + //FIXME: handle quoting in the script + Console.WriteLine ("Found msbuild script in PATH: {0}", msbuildInPath); + var tokens = File.ReadAllText (msbuildInPath).Split (new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + var filename = tokens.FirstOrDefault (t => t.EndsWith ("MSBuild.dll", StringComparison.OrdinalIgnoreCase)); + if (filename != null && File.Exists (filename)) { + var dir = Path.GetDirectoryName (filename); + Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath (dir); + Console.WriteLine ("Discovered MSBuild from launch script: {0}", dir); + return; + } + } + + foreach (var dir in GetPossibleMSBuildDirectoriesLinux ()) { + if (File.Exists (Path.Combine (dir, "MSBuild.dll"))) { + Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath (dir); + Console.WriteLine ("Discovered MSBuild at well known location: {0}", dir); + return; + } + } + + throw new Exception ("Could not find MSBuild"); + } + + static IEnumerable GetPossibleMSBuildDirectoriesLinux () + { + yield return "/usr/lib/mono/msbuild/Current/bin"; + yield return "/usr/lib/mono/msbuild/15.0/bin"; + } + + static string FindInPath (string name) + { + var pathEnv = Environment.GetEnvironmentVariable ("PATH"); + if (pathEnv == null) { + return null; + } + + var paths = pathEnv.Split (new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in paths) { + var possible = Path.Combine (path, name); + if (File.Exists (possible)) { + return possible; + } + } + + return null; + } + */ +} diff --git a/MonoDevelop.MSBuild.Tests/NUnitExtensions.cs b/MonoDevelop.MSBuild.Tests/Helpers/NUnitExtensions.cs similarity index 100% rename from MonoDevelop.MSBuild.Tests/NUnitExtensions.cs rename to MonoDevelop.MSBuild.Tests/Helpers/NUnitExtensions.cs diff --git a/MonoDevelop.MSBuild.Tests/Analyzers/TestSchemaProvider.cs b/MonoDevelop.MSBuild.Tests/Helpers/TestSchemaProvider.cs similarity index 89% rename from MonoDevelop.MSBuild.Tests/Analyzers/TestSchemaProvider.cs rename to MonoDevelop.MSBuild.Tests/Helpers/TestSchemaProvider.cs index 614a6b53..4877acbb 100644 --- a/MonoDevelop.MSBuild.Tests/Analyzers/TestSchemaProvider.cs +++ b/MonoDevelop.MSBuild.Tests/Helpers/TestSchemaProvider.cs @@ -1,12 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; using System.IO; - using MonoDevelop.MSBuild.Schema; -namespace MonoDevelop.MSBuild.Tests.Analyzers +namespace MonoDevelop.MSBuild.Tests.Helpers { class TestSchemaProvider : MSBuildSchemaProvider { diff --git a/MonoDevelop.MSBuild.Tests/MSBuildImportEvaluationTests.cs b/MonoDevelop.MSBuild.Tests/MSBuildImportEvaluationTests.cs index 68bfad35..c67d2ff7 100644 --- a/MonoDevelop.MSBuild.Tests/MSBuildImportEvaluationTests.cs +++ b/MonoDevelop.MSBuild.Tests/MSBuildImportEvaluationTests.cs @@ -7,7 +7,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; @@ -16,7 +15,7 @@ using MonoDevelop.MSBuild.Language.Expressions; using MonoDevelop.MSBuild.Language.Typesystem; using MonoDevelop.MSBuild.Schema; - +using MonoDevelop.MSBuild.Tests.Helpers; using MonoDevelop.Xml.Parser; using MonoDevelop.Xml.Tests; diff --git a/MonoDevelop.MSBuild.Tests/MSBuildTestHelpers.cs b/MonoDevelop.MSBuild.Tests/MSBuildTestHelpers.cs deleted file mode 100644 index 4e3734ec..00000000 --- a/MonoDevelop.MSBuild.Tests/MSBuildTestHelpers.cs +++ /dev/null @@ -1,170 +0,0 @@ -// -// Copyright (c) 2017 Microsoft Corp. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; - -using MonoDevelop.MSBuild.Language; -using MonoDevelop.MSBuild.Util; - -using MonoDevelop.Xml.Parser; -using MonoDevelop.Xml.Tests; - -namespace MonoDevelop.MSBuild.Tests -{ - static class MSBuildTestHelpers - { - const char defaultMarker = '|'; - - public static IEnumerable<(int index, T result)> SelectAtMarkers ( - string docString, - Func<(XmlSpineParser parser, ITextSource textSource, MSBuildDocument doc, int offset), T> selector, - string filename = null, - char marker = defaultMarker) - => SelectAtMarkers (TextWithMarkers.Parse (docString, marker), selector, filename); - - public static IEnumerable<(int index, T result)> SelectAtMarkers ( - TextWithMarkers text, - Func<(XmlSpineParser parser, ITextSource textSource, MSBuildDocument doc, int offset), T> selector, - string filename = null, - char? marker = null) - { - var indices = new Queue (text.GetMarkedPositions (marker)); - - var textDoc = new StringTextSource (text.Text); - - var treeParser = new XmlTreeParser (new XmlRootState ()); - var (xdoc, _) = treeParser.Parse (textDoc.CreateReader ()); - var logger = TestLoggerFactory.CreateTestMethodLogger (); - var parseContext = new MSBuildParserContext ( - new NullMSBuildEnvironment (), null, null, null, filename ?? "test.csproj", new PropertyValueCollector (false), null, logger, null, default); - var doc = CreateEmptyDocument (); - doc.Build (xdoc, parseContext); - - var parser = new XmlSpineParser (treeParser.RootState); - - var nextIndex = indices.Dequeue (); - for (int i = 0; i < textDoc.Length; i++) { - parser.Push (textDoc[i]); - if (parser.Position != nextIndex) { - continue; - } - - yield return (i, selector ((parser, textDoc, doc, i))); - - if (indices.Count == 0) { - break; - } - nextIndex = indices.Dequeue (); - } - } - - internal static MSBuildDocument CreateEmptyDocument () - { - return new MSBuildDocument (null, false); - } - - static bool registeredAssemblies; - - public static void RegisterMSBuildAssemblies () - { - if (registeredAssemblies) { - return; - } - registeredAssemblies = true; - -#if NETFRAMEWORK - if (Platform.IsWindows) { - var vs17Instance = Microsoft.Build.Locator.MSBuildLocator.QueryVisualStudioInstances () - .FirstOrDefault (x => x.DiscoveryType == Microsoft.Build.Locator.DiscoveryType.VisualStudioSetup && x.Version.Major >= 17); - if (vs17Instance == null) { - throw new InvalidOperationException ("Did not find instance of Visual Studio 17.0 or later"); - } - Microsoft.Build.Locator.MSBuildLocator.RegisterInstance (vs17Instance); - return; - } -#endif - var dotnetInstance = Microsoft.Build.Locator.MSBuildLocator.QueryVisualStudioInstances () - .FirstOrDefault (x => x.DiscoveryType == Microsoft.Build.Locator.DiscoveryType.DotNetSdk && x.Version.Major >= 6.0); - if (dotnetInstance == null) { - throw new InvalidOperationException ("Did not find instance of .NET 6.0 or later"); - } - Microsoft.Build.Locator.MSBuildLocator.RegisterInstance (dotnetInstance); - return; - } - - /* - // might need this again, keep it around for now - void FindMSBuildInPath () - { - var msbuildInPath = FindInPath ("msbuild"); - if (msbuildInPath != null) { - //attempt to read the msbuild.dll location from the launch script - //FIXME: handle quoting in the script - Console.WriteLine ("Found msbuild script in PATH: {0}", msbuildInPath); - var tokens = File.ReadAllText (msbuildInPath).Split (new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - var filename = tokens.FirstOrDefault (t => t.EndsWith ("MSBuild.dll", StringComparison.OrdinalIgnoreCase)); - if (filename != null && File.Exists (filename)) { - var dir = Path.GetDirectoryName (filename); - Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath (dir); - Console.WriteLine ("Discovered MSBuild from launch script: {0}", dir); - return; - } - } - - foreach (var dir in GetPossibleMSBuildDirectoriesLinux ()) { - if (File.Exists (Path.Combine (dir, "MSBuild.dll"))) { - Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath (dir); - Console.WriteLine ("Discovered MSBuild at well known location: {0}", dir); - return; - } - } - - throw new Exception ("Could not find MSBuild"); - } - - static IEnumerable GetPossibleMSBuildDirectoriesLinux () - { - yield return "/usr/lib/mono/msbuild/Current/bin"; - yield return "/usr/lib/mono/msbuild/15.0/bin"; - } - - static string FindInPath (string name) - { - var pathEnv = Environment.GetEnvironmentVariable ("PATH"); - if (pathEnv == null) { - return null; - } - - var paths = pathEnv.Split (new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); - foreach (var path in paths) { - var possible = Path.Combine (path, name); - if (File.Exists (possible)) { - return possible; - } - } - - return null; - } - */ - } -} diff --git a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticSeverity.cs b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticSeverity.cs index bb1faeb0..5147ea1f 100644 --- a/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticSeverity.cs +++ b/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticSeverity.cs @@ -10,8 +10,11 @@ namespace MonoDevelop.MSBuild.Analysis public enum MSBuildDiagnosticSeverity { None = 0, + Suggestion = 1 << 0, Warning = 1 << 1, - Error = 1 << 2 + Error = 1 << 2, + + All = Suggestion | Warning | Error } } \ No newline at end of file diff --git a/MonoDevelop.MSBuild/Util/TextWithMarkers.cs b/MonoDevelop.MSBuild/Util/TextWithMarkers.cs index 92b30494..5e7ed9f4 100644 --- a/MonoDevelop.MSBuild/Util/TextWithMarkers.cs +++ b/MonoDevelop.MSBuild/Util/TextWithMarkers.cs @@ -70,14 +70,19 @@ public TextSpan GetMarkedSpan (char? markerChar = null) var id = GetMarkerId (markerChar); var positions = markedPositionsById[id]; - if (positions.Count != 2) { - throw new ArgumentException ($"Found {positions.Count} markers for char '{markerChars[id]}', must have exactly 2 to treat as a span", nameof (markerChar)); + // treat single marker as zero width span + if (positions.Count == 1) { + int pos = positions[0]; + return TextSpan.FromBounds (pos, pos); } - int start = positions[0]; - int end = positions[1]; + if (positions.Count == 2) { + int start = positions[0]; + int end = positions[1]; + return TextSpan.FromBounds (start, end); + } - return TextSpan.FromBounds (start, end); + throw new ArgumentException ($"Found {positions.Count} markers for char '{markerChars[id]}', must have exactly 1 or 2 markers to treat as a span", nameof (markerChar)); } public TextSpan[] GetMarkedSpans (char? markerChar = null) From c94ce4933198b65c76a1cc4e27ccec73804d13b9 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 9 Mar 2024 01:05:48 -0500 Subject: [PATCH 3/4] Add tests for TargetFrameworks/RuntimeIdentifiers pluralization code fix --- ...MultitargetingPluralizationCodeFixTests.cs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 MonoDevelop.MSBuild.Tests.Editor/CodeFixes/FixMultitargetingPluralizationCodeFixTests.cs diff --git a/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/FixMultitargetingPluralizationCodeFixTests.cs b/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/FixMultitargetingPluralizationCodeFixTests.cs new file mode 100644 index 00000000..a0afe6f9 --- /dev/null +++ b/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/FixMultitargetingPluralizationCodeFixTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Threading.Tasks; + +using MonoDevelop.MSBuild.Analyzers; +using MonoDevelop.MSBuild.Editor.CodeFixes; + +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace MonoDevelop.MSBuild.Tests.Editor.CodeFixes; + +[TestFixture] +class FixMultitargetingPluralizationCodeFixTests : MSBuildEditorTest +{ + [Test] + public Task DepluralizeTargetFrameworks () + { + return this.TestCodeFix ( +@" + + net8.0 + + +", + "Change 'TargetFrameworks' to 'TargetFramework'", + 1, +@" + + net8.0 + + +"); + } + + [Test] + public Task PluralizeTargetFramework () + { + return this.TestCodeFix ( +@" + + net48;net8.0 + + +", + "Change 'TargetFramework' to 'TargetFrameworks'", + 1, +@" + + net48;net8.0 + + +"); + } + + [Test] + public Task DepluralizeRuntimeIdentifiers () + { + return this.TestCodeFix ( +@" + + windows-x64 + + +", + "Change 'RuntimeIdentifiers' to 'RuntimeIdentifier'", + 1, +@" + + windows-x64 + + +"); + } + [Test] + public Task PluralizeRuntimeIdentifier () + { + return this.TestCodeFix ( +@" + + windows-x64;linux-x64 + + +", + "Change 'RuntimeIdentifier' to 'RuntimeIdentifiers'", + 1, +@" + + windows-x64;linux-x64 + + +"); + } +} \ No newline at end of file From 9c4c7c9092f26557946350f2574032841dae90a1 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 9 Mar 2024 01:06:07 -0500 Subject: [PATCH 4/4] Add test for AppendNoWarn code fix --- .../CodeFixes/AppendNoWarnCodeFixTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 MonoDevelop.MSBuild.Tests.Editor/CodeFixes/AppendNoWarnCodeFixTest.cs diff --git a/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/AppendNoWarnCodeFixTest.cs b/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/AppendNoWarnCodeFixTest.cs new file mode 100644 index 00000000..c2250e5f --- /dev/null +++ b/MonoDevelop.MSBuild.Tests.Editor/CodeFixes/AppendNoWarnCodeFixTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using System.Threading.Tasks; + +using MonoDevelop.MSBuild.Analyzers; +using MonoDevelop.MSBuild.Editor.CodeFixes; + +using NUnit.Framework; +using NUnit.Framework.Internal; + +namespace MonoDevelop.MSBuild.Tests.Editor.CodeFixes; + +[TestFixture] +class AppendNoWarnCodeFixTests : MSBuildEditorTest +{ + [Test] + public Task TestFixAppendNoWarn () + { + return this.TestCodeFix ( +@" + + CS1234;CS456 + + +", + "Prepend '$(NoWarn)' to list", + 1, +@" + + $(NoWarn);CS1234;CS456 + + +"); + } +} \ No newline at end of file