Родитель
902d37f0da
Коммит
cf64a5bd79
|
@ -5,7 +5,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiffPlex;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
||||
{
|
||||
|
@ -19,9 +18,9 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultTextMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="differ">The IDiffer implementation to use for determining which text replacements correspond with original text in ambiguous cases.</param>
|
||||
/// <param name="chunker">The IChunker to be used with the differ.</param>
|
||||
/// </summary>
|
||||
public DefaultTextMatcher(IDiffer differ, IChunker chunker)
|
||||
{
|
||||
_chunker = chunker ?? throw new ArgumentNullException(nameof(chunker));
|
||||
|
@ -60,13 +59,13 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
// If both groups have the same number of elements, then they pair in order
|
||||
if (originalTexts.Count() == newTexts.Count())
|
||||
{
|
||||
AddReplacementCandidates(replacements, originalTexts.Zip(newTexts, (original, updated) => new TextReplacement(original, SourceText.From(updated))));
|
||||
AddReplacementCandidates(replacements, originalTexts.Zip(newTexts, (original, updated) => new TextReplacement(original, updated)));
|
||||
}
|
||||
|
||||
// If there are no new texts, then the original elements all pair with empty source text
|
||||
else if (!newTexts.Any())
|
||||
{
|
||||
AddReplacementCandidates(replacements, originalTexts.Select(t => new TextReplacement(t, SourceText.From(string.Empty))));
|
||||
AddReplacementCandidates(replacements, originalTexts.Select(t => new TextReplacement(t, string.Empty)));
|
||||
}
|
||||
|
||||
// This is the tricky one. If there are less updated code blocks than original code blocks, it will be necesary to guess which original code blocks
|
||||
|
@ -85,17 +84,17 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// </summary>
|
||||
private static void AddReplacementCandidates(List<TextReplacement> replacements, IEnumerable<TextReplacement> candidates)
|
||||
{
|
||||
replacements.AddRange(candidates.Where(c => !c.NewText.ContentEquals(c.OriginalText)));
|
||||
replacements.AddRange(candidates.Where(c => !c.NewText.Equals(c.OriginalText, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively enumerate all the possible ways the source and updated texts can match
|
||||
/// Recursively enumerate all the possible ways the source and updated texts can match.
|
||||
/// </summary>
|
||||
private static IEnumerable<IEnumerable<TextReplacement>> GetAllPossiblePairings(IEnumerable<MappedSubText> originalTexts, IEnumerable<string> newTexts)
|
||||
{
|
||||
if (originalTexts.Count() == newTexts.Count())
|
||||
{
|
||||
yield return originalTexts.Zip(newTexts, (original, updated) => new TextReplacement(original, SourceText.From(updated)));
|
||||
yield return originalTexts.Zip(newTexts, (original, updated) => new TextReplacement(original, updated));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -112,7 +111,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the total size of diff (sum or all inserts and deletes) between old and new texts in an enumerable of text replacements
|
||||
/// Finds the total size of diff (sum or all inserts and deletes) between old and new texts in an enumerable of text replacements.
|
||||
/// </summary>
|
||||
private int GetTotalDiffSize(IEnumerable<TextReplacement> replacements) =>
|
||||
replacements.Sum(r =>
|
||||
|
|
|
@ -7,6 +7,6 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
{
|
||||
public interface ITextReplacer
|
||||
{
|
||||
void ApplyTextReplacements(IList<TextReplacement> replacements);
|
||||
void ApplyTextReplacements(IEnumerable<TextReplacement> replacements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,14 +86,14 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
public bool Equals(MappedSubText other) =>
|
||||
other != null &&
|
||||
Text.ContentEquals(other.Text) &&
|
||||
SourceLocation.Equals(other.SourceLocation, StringComparison.Ordinal);
|
||||
SourceLocation.Equals(other.SourceLocation, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = default(HashCode);
|
||||
hashcode.Add(Text.ToString(), StringComparer.Ordinal);
|
||||
hashcode.Add(SourceLocation, StringComparer.Ordinal);
|
||||
hashcode.Add(SourceLocation, StringComparer.OrdinalIgnoreCase);
|
||||
return hashcode.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,10 +147,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
|
||||
// Identify changed code sections
|
||||
var textReplacements = await GetReplacements(originalProject, project.Documents.Where(d => generatedFilePaths.Contains(d.FilePath)), token).ConfigureAwait(false);
|
||||
var z = project.Documents.Where(d => generatedFilePaths.Contains(d.FilePath)).Select(d => d.GetTextAsync().Result.ToString()).ToArray();
|
||||
|
||||
// Update cshtml based on changes made to generated source code
|
||||
// These are applied after finding all of them so that they can be applied in reverse line order
|
||||
_logger.LogDebug("Applying {ReplacemenCount} updates to Razor documents based on changes made by code fix providers", textReplacements.Count);
|
||||
_logger.LogDebug("Applying {ReplacementCount} updates to Razor documents based on changes made by code fix providers", textReplacements.Count());
|
||||
_textReplacer.ApplyTextReplacements(textReplacements);
|
||||
await FixUpProjectFileAsync(context, token).ConfigureAwait(false);
|
||||
|
||||
|
@ -162,7 +163,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
return new FileUpdaterResult(true, textReplacements.Select(r => r.FilePath).Distinct());
|
||||
}
|
||||
|
||||
private async Task<IList<TextReplacement>> GetReplacements(Project originalProject, IEnumerable<Document> updatedDocuments, CancellationToken token)
|
||||
private async Task<IEnumerable<TextReplacement>> GetReplacements(Project originalProject, IEnumerable<Document> updatedDocuments, CancellationToken token)
|
||||
{
|
||||
var replacements = new List<TextReplacement>();
|
||||
foreach (var updatedDocument in updatedDocuments)
|
||||
|
@ -185,7 +186,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
}
|
||||
}
|
||||
|
||||
return replacements;
|
||||
return replacements.Distinct();
|
||||
}
|
||||
|
||||
private static string GetGeneratedFilePath(RazorCodeDocument doc) => $"{doc.Source.FilePath}.cs";
|
||||
|
|
|
@ -16,15 +16,15 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// </summary>
|
||||
public class RazorTextReplacer : ITextReplacer
|
||||
{
|
||||
private static readonly Regex UsingBlockRegex = new Regex(@"^(\s*using\s+(?<namespace>.+?);+\s*)+$", RegexOptions.Compiled);
|
||||
private static readonly Regex UsingNamespaceRegex = new Regex(@"using\s+(?<namespace>.+?);", RegexOptions.Compiled);
|
||||
private static readonly Regex UsingBlockRegex = new(@"^(\s*using\s+(?<namespace>.+?);+\s*)+$", RegexOptions.Compiled);
|
||||
private static readonly Regex UsingNamespaceRegex = new(@"using\s+(?<namespace>.+?);", RegexOptions.Compiled);
|
||||
|
||||
private readonly ILogger<RazorTextReplacer> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RazorTextReplacer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="logger">Logger for logging diagnostics.</param>
|
||||
public RazorTextReplacer(ILogger<RazorTextReplacer> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
@ -34,36 +34,57 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// Updates source code in Razor documents based on provider TextReplacements, accounting for Razor source code transition syntax.
|
||||
/// </summary>
|
||||
/// <param name="replacements">The text replacements to apply.</param>
|
||||
public void ApplyTextReplacements(IList<TextReplacement> replacements)
|
||||
public void ApplyTextReplacements(IEnumerable<TextReplacement> replacements)
|
||||
{
|
||||
if (replacements is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(replacements));
|
||||
}
|
||||
|
||||
// Load each file with replacements into memory and update based on replacements
|
||||
var replacementsByFile = replacements.Distinct().OrderByDescending(t => t.StartingLine).GroupBy(t => t.FilePath);
|
||||
foreach (var replacementGroup in replacementsByFile)
|
||||
{
|
||||
// Read the document as lines instead of all as one string because replacements
|
||||
// include line offsets.
|
||||
var documentLines = File.ReadAllLines(replacementGroup.Key);
|
||||
var documentText = new StringBuilder();
|
||||
foreach (var line in documentLines)
|
||||
{
|
||||
documentText.AppendLine(line);
|
||||
}
|
||||
var documentTextStr = File.ReadAllText(replacementGroup.Key);
|
||||
var lineOffsets = GetLineOffsets(documentTextStr).ToArray();
|
||||
var documentText = new StringBuilder(documentTextStr);
|
||||
|
||||
foreach (var replacement in replacementGroup)
|
||||
{
|
||||
_logger.LogInformation("Updating source code in Razor document {FilePath} at line {Line}", replacement.FilePath, replacement.StartingLine);
|
||||
|
||||
// If the original text doesn't fit in the lines of the original document, then the replacement is invalid
|
||||
if (replacement.StartingLine + GetLineCount(replacement.OriginalText) >= lineOffsets.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start looking for replacements at the start of the indicated line
|
||||
var startOffset = GetLineOffset(documentLines, replacement.StartingLine);
|
||||
var startOffset = lineOffsets[replacement.StartingLine];
|
||||
|
||||
// Stop looking for replacements at the start of the first line after the indicated line plus the number of lines in the indicated text
|
||||
var endOffset = GetLineOffset(documentLines, replacement.StartingLine + replacement.OriginalText.Lines.Count);
|
||||
var endOffset = lineOffsets[replacement.StartingLine + GetLineCount(replacement.OriginalText)];
|
||||
|
||||
// Trim the string that's being replaced because code from Razor code blocks will include a couple extra spaces (to make room for @{)
|
||||
// compared to the source that actually appeared in the cshtml file.
|
||||
var originalText = replacement.OriginalText.ToString().TrimStart();
|
||||
var updatedText = replacement.NewText.ToString().TrimStart();
|
||||
MinimizeReplacement(ref originalText, ref updatedText);
|
||||
|
||||
// Generally, it's not necessary to minimize replacements since the text should be the same in both documents
|
||||
// However, in the specific case of text being added before the first #line pragma in a Razor doc, it's possible
|
||||
// that C# unrelated to the original document will show up with the replaced text, so minimize replacements only
|
||||
// in the case that text is being inserted before the original first line.
|
||||
if (replacement.StartingLine == 0)
|
||||
{
|
||||
MinimizeReplacement(ref originalText, ref updatedText);
|
||||
}
|
||||
|
||||
// If the changed text ends with a semi-colon, trim it since the semi-colon won't appear in implicit Razor expressions
|
||||
if (originalText.Trim().EndsWith(";", StringComparison.Ordinal) && updatedText.Trim().EndsWith(";", StringComparison.Ordinal))
|
||||
{
|
||||
originalText = originalText.Trim().Trim(';');
|
||||
updatedText = updatedText.Trim().Trim(';');
|
||||
}
|
||||
|
||||
// If new text is being added, insert it with correct Razor transition syntax
|
||||
if (string.IsNullOrWhiteSpace(originalText))
|
||||
|
@ -92,8 +113,12 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
{
|
||||
var implicitExpression = $"@{originalText.Replace(";", string.Empty)}";
|
||||
var explicitExpression = $"@({originalText.Replace(";", string.Empty).Trim()})";
|
||||
|
||||
documentText.Replace(implicitExpression, updatedText, startOffset, endOffset - startOffset);
|
||||
endOffset = Math.Min(endOffset, documentText.Length);
|
||||
|
||||
documentText.Replace(explicitExpression, updatedText, startOffset, endOffset - startOffset);
|
||||
endOffset = Math.Min(endOffset, documentText.Length);
|
||||
}
|
||||
|
||||
documentText.Replace(originalText, updatedText, startOffset, endOffset - startOffset);
|
||||
|
@ -104,22 +129,33 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
}
|
||||
}
|
||||
|
||||
private static int GetLineOffset(string[] lines, int startingLine)
|
||||
{
|
||||
var offset = 0;
|
||||
private static int GetLineCount(string input) => input.Split('\n').Length;
|
||||
|
||||
for (var i = 1; i < startingLine && i <= lines.Length; i++)
|
||||
private static IEnumerable<int> GetLineOffsets(string text)
|
||||
{
|
||||
// Pre-line 1
|
||||
yield return 0;
|
||||
|
||||
// Line 1
|
||||
if (text.Any())
|
||||
{
|
||||
// StreamSourceDoc.Lines is 0-based but line directives (as used in MappedSubText) are 1-based,
|
||||
// so subtract one from i.
|
||||
offset += lines[i - 1].Length + Environment.NewLine.Length;
|
||||
yield return 0;
|
||||
}
|
||||
|
||||
return offset;
|
||||
// Subsequent lines
|
||||
var index = text.IndexOf('\n');
|
||||
while (index > -1)
|
||||
{
|
||||
yield return index + 1;
|
||||
index = text.IndexOf('\n', index + 1);
|
||||
}
|
||||
|
||||
// EOF
|
||||
yield return text.Length;
|
||||
}
|
||||
|
||||
// Removes leading and trailing portions of original and updated that are the same
|
||||
private static void MinimizeReplacement(ref string original, ref string? updated)
|
||||
private static void MinimizeReplacement(ref string original, ref string updated)
|
||||
{
|
||||
if (updated is null)
|
||||
{
|
||||
|
|
|
@ -12,8 +12,8 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
{
|
||||
internal class RazorUpdaterSubStep : UpgradeStep
|
||||
{
|
||||
private RazorUpdaterStep _razorUpdaterStep;
|
||||
private IUpdater<RazorCodeDocument> _updater;
|
||||
private readonly RazorUpdaterStep _razorUpdaterStep;
|
||||
private readonly IUpdater<RazorCodeDocument> _updater;
|
||||
|
||||
public override string Id => _updater.Id;
|
||||
|
||||
|
@ -103,6 +103,8 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// <summary>
|
||||
/// Apply upgrade and update Status as necessary.
|
||||
/// </summary>
|
||||
/// <param name="context">The upgrade context to apply this step to.</param>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
/// <returns>True if the upgrade step was successfully applied or false if upgrade failed.</returns>
|
||||
public override async Task<bool> ApplyAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
||||
{
|
||||
|
@ -25,20 +24,20 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// <summary>
|
||||
/// Gets the original text that is to be replaced.
|
||||
/// </summary>
|
||||
public SourceText OriginalText { get; }
|
||||
public string OriginalText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new text to replace the original texts with.
|
||||
/// </summary>
|
||||
public SourceText NewText { get; }
|
||||
public string NewText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextReplacement"/> class, getting the original text, file path, and starting line from an original MappedSubText.
|
||||
/// </summary>
|
||||
/// <param name="originalText">A MappedSubText containing the original text and original text location.</param>
|
||||
/// <param name="newText">The new text to replace the original text with.</param>
|
||||
public TextReplacement(MappedSubText originalText, SourceText newText)
|
||||
: this(originalText?.Text ?? throw new ArgumentNullException(nameof(originalText)), newText, originalText.FilePath, originalText.StartingLine)
|
||||
public TextReplacement(MappedSubText originalText, string newText)
|
||||
: this(originalText?.Text.ToString() ?? throw new ArgumentNullException(nameof(originalText)), newText, originalText.FilePath, originalText.StartingLine)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
|
@ -48,7 +47,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// <param name="newText">The text to replace the original text with.</param>
|
||||
/// <param name="filePath">The path to the file text should be replaced in.</param>
|
||||
/// <param name="startingLine">The line number the original text starts on.</param>
|
||||
public TextReplacement(SourceText originalText, SourceText newText, string filePath, int startingLine)
|
||||
public TextReplacement(string originalText, string newText, string filePath, int startingLine)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
|
@ -68,8 +67,10 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
/// <returns>True of the other TextReplacement is equal to this one, false otherwise.</returns>
|
||||
public bool Equals(TextReplacement? other) =>
|
||||
other != null &&
|
||||
NewText.ContentEquals(other.NewText) &&
|
||||
OriginalText.Equals(other.OriginalText);
|
||||
FilePath.Equals(other.FilePath, StringComparison.OrdinalIgnoreCase) &&
|
||||
StartingLine == other.StartingLine &&
|
||||
NewText.Equals(other.NewText, StringComparison.Ordinal) &&
|
||||
OriginalText.Equals(other.OriginalText, StringComparison.Ordinal);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as TextReplacement);
|
||||
|
@ -78,8 +79,10 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor
|
|||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = default(HashCode);
|
||||
hashcode.Add(NewText.ToString(), StringComparer.Ordinal);
|
||||
hashcode.Add(OriginalText);
|
||||
hashcode.Add(StartingLine);
|
||||
hashcode.Add(FilePath, StringComparer.OrdinalIgnoreCase);
|
||||
hashcode.Add(NewText, StringComparer.Ordinal);
|
||||
hashcode.Add(OriginalText, StringComparer.Ordinal);
|
||||
return hashcode.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Backup.Tests
|
|||
}
|
||||
|
||||
private static UpgradeOptions GetDefaultNonInteractiveOptions() =>
|
||||
new UpgradeOptions
|
||||
new()
|
||||
{
|
||||
NonInteractive = true,
|
||||
NonInteractiveWait = 0,
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiffPlex;
|
||||
using DiffPlex.Chunkers;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public class DefaultTextMatcherTests
|
||||
{
|
||||
[Fact]
|
||||
public void CtorNegativeTests()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>("differ", () => new DefaultTextMatcher(null!, new CharacterChunker()));
|
||||
Assert.Throws<ArgumentNullException>("chunker", () => new DefaultTextMatcher(new Differ(), null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchOrderedSubTextsNegativeTests()
|
||||
{
|
||||
var matcher = new DefaultTextMatcher(new Differ(), new CharacterChunker());
|
||||
var originalTexts = new MappedSubText[]
|
||||
{
|
||||
GetSubText("Test", "test.txt", 1)
|
||||
};
|
||||
var updatedTexts = new string[] { "Test1", "Test2" };
|
||||
|
||||
Assert.Throws<ArgumentNullException>("originalTexts", () => matcher.MatchOrderedSubTexts(null!, updatedTexts));
|
||||
Assert.Throws<ArgumentNullException>("newTexts", () => matcher.MatchOrderedSubTexts(originalTexts, null!));
|
||||
Assert.Throws<ArgumentException>(() => matcher.MatchOrderedSubTexts(originalTexts, updatedTexts));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MatchOrderedSubTextsData))]
|
||||
public void MatchOrderedSubTextsPositiveTests(IEnumerable<MappedSubText> originalTexts, IEnumerable<string> newTexts, IEnumerable<TextReplacement> expectedReplacements)
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new DefaultTextMatcher(new Differ(), new CharacterChunker());
|
||||
|
||||
// Act
|
||||
var replacements = matcher.MatchOrderedSubTexts(originalTexts, newTexts);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(replacements, expectedReplacements.Select<TextReplacement, Action<TextReplacement>>(expected => actual => Assert.Equal(expected, actual)).ToArray());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> MatchOrderedSubTextsData
|
||||
{
|
||||
get
|
||||
{
|
||||
// No original or updated texts
|
||||
yield return new object[]
|
||||
{
|
||||
Array.Empty<MappedSubText>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<TextReplacement>()
|
||||
};
|
||||
|
||||
// Equal number of original and updated texts
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("A", "A.txt", 10), GetSubText("B", "B.txt", 2), GetSubText("B", "B.txt", 2) },
|
||||
new string[] { "C", string.Empty, "D" },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("A", "C", "A.txt", 10),
|
||||
new TextReplacement("B", string.Empty, "B.txt", 2),
|
||||
new TextReplacement("B", "D", "B.txt", 2),
|
||||
}
|
||||
};
|
||||
|
||||
// All texts removed
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("A", "A.txt", 10), GetSubText("B", "B.txt", 2), GetSubText("C", "B.txt", 2) },
|
||||
Array.Empty<string>(),
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("A", string.Empty, "A.txt", 10),
|
||||
new TextReplacement("B", string.Empty, "B.txt", 2),
|
||||
new TextReplacement("C", string.Empty, "B.txt", 2),
|
||||
}
|
||||
};
|
||||
|
||||
// Unchange texts aren't returned
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("A", "A.txt", 10), GetSubText("B", "B.txt", 2), GetSubText("C", "C.txt", 0) },
|
||||
new string[] { "D", "B", "C " },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("A", "D", "A.txt", 10),
|
||||
new TextReplacement("C", "C ", "C.txt", 0),
|
||||
}
|
||||
};
|
||||
|
||||
// Some texts removed
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("Cat", "A.txt", 10), GetSubText("Dog", "B.txt", 2), GetSubText("Fish", "C.txt", 1000000000) },
|
||||
new string[] { "C", "F" },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Cat", "C", "A.txt", 10),
|
||||
new TextReplacement("Dog", string.Empty, "B.txt", 2),
|
||||
new TextReplacement("Fish", "F", "C.txt", 1000000000),
|
||||
}
|
||||
};
|
||||
|
||||
// Some texts removed (but pair with different updated text)
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("Cat", "A.txt", 10), GetSubText("Dog", "B.txt", 2), GetSubText("Fish", "C.txt", 1000000000) },
|
||||
new string[] { "C", "o" },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Cat", "C", "A.txt", 10),
|
||||
new TextReplacement("Dog", "o", "B.txt", 2),
|
||||
new TextReplacement("Fish", string.Empty, "C.txt", 1000000000),
|
||||
}
|
||||
};
|
||||
|
||||
// Some texts removed (but pair with different updated text)
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("Cat", "A.txt", 10), GetSubText("Dog", "B.txt", 2), GetSubText("Fish", "C.txt", 1000000000) },
|
||||
new string[] { "CDg", "-" },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Cat", string.Empty, "A.txt", 10),
|
||||
new TextReplacement("Dog", "CDg", "B.txt", 2),
|
||||
new TextReplacement("Fish", "-", "C.txt", 1000000000),
|
||||
}
|
||||
};
|
||||
|
||||
// Original texts can't be reordered
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[] { GetSubText("Cat", "A.txt", 10), GetSubText("Dog", "B.txt", 2), GetSubText("Fish", "C.txt", 1000000000) },
|
||||
new string[] { "Fish", "Dog" },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Cat", "Fish", "A.txt", 10),
|
||||
new TextReplacement("Fish", string.Empty, "C.txt", 1000000000),
|
||||
}
|
||||
};
|
||||
|
||||
// Multiple texts removed; also check that file path comparison is case-insensitive
|
||||
yield return new object[]
|
||||
{
|
||||
new MappedSubText[]
|
||||
{
|
||||
GetSubText("Cat", "A.txt", 10),
|
||||
GetSubText("Dog", "B.txt", 2),
|
||||
GetSubText("Fish", "C.txt", 1000000000),
|
||||
GetSubText("Bird Bird", "D.txt", 11),
|
||||
GetSubText(" ", "E.txt", 2),
|
||||
},
|
||||
new string[] { "Cog", " " },
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Cat", string.Empty, "a.txt", 10),
|
||||
new TextReplacement("Dog", "Cog", "b.txt", 2),
|
||||
new TextReplacement("Fish", string.Empty, "c.txt", 1000000000),
|
||||
new TextReplacement("Bird Bird", string.Empty, "d.txt", 11),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static MappedSubText GetSubText(string text, string filePath, int startingLine) =>
|
||||
new MappedSubText(SourceText.From(text), filePath, startingLine);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public record LocationLookup(string Path, string? Keyword, int StartOffset = 0, int EndOffset = 0);
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Auto-generated by Razor engine
|
||||
|
||||
namespace Razor
|
||||
{
|
||||
#line hidden
|
||||
#line 1 "test.cshtml"
|
||||
using System;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
[global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"SHA1", @"38e4e7b9907aaf0dbd917f25e1c2da15895aa64f", @"/test.cshtml")]
|
||||
public class Template
|
||||
{
|
||||
#line hidden
|
||||
#pragma warning disable 0169
|
||||
private string __tagHelperStringValueBuffer;
|
||||
#pragma warning restore 0169
|
||||
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext;
|
||||
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner();
|
||||
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null;
|
||||
private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (__backed__tagHelperScopeManager == null)
|
||||
{
|
||||
__backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope);
|
||||
}
|
||||
return __backed__tagHelperScopeManager;
|
||||
}
|
||||
}
|
||||
private global::PTagHelper __PTagHelper;
|
||||
#pragma warning disable 1998
|
||||
public async override global::System.Threading.Tasks.Task ExecuteAsync()
|
||||
{
|
||||
#line 3 "test.cshtml"
|
||||
|
||||
var foo = "Hello World!";
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
WriteLiteral("\n");
|
||||
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("p", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "38e4e7b9907aaf0dbd917f25e1c2da15895aa64f2096", async () => {
|
||||
WriteLiteral("Content");
|
||||
}
|
||||
);
|
||||
__PTagHelper = CreateTagHelper<global::PTagHelper>();
|
||||
__tagHelperExecutionContext.Add(__PTagHelper);
|
||||
#line 7 "test.cshtml"
|
||||
__PTagHelper.FooProp = 123;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
__tagHelperExecutionContext.AddTagHelperAttribute("foo", __PTagHelper.FooProp, global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes);
|
||||
BeginWriteTagHelperAttribute();
|
||||
#line 7 "test.cshtml"
|
||||
WriteLiteral(foo);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
__tagHelperStringValueBuffer = EndWriteTagHelperAttribute();
|
||||
__PTagHelper.BarProp = __tagHelperStringValueBuffer;
|
||||
__tagHelperExecutionContext.AddTagHelperAttribute("bar", __PTagHelper.BarProp, global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.SingleQuotes);
|
||||
await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
|
||||
if (!__tagHelperExecutionContext.Output.IsContentModified)
|
||||
{
|
||||
await __tagHelperExecutionContext.SetOutputContentAsync();
|
||||
}
|
||||
Write(__tagHelperExecutionContext.Output);
|
||||
__tagHelperExecutionContext = __tagHelperScopeManager.End();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
|
@ -0,0 +1,133 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public class MappedSubTextTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(EqualsTestData))]
|
||||
public void EqualsTests(MappedSubText a, MappedSubText b, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, a.Equals(b));
|
||||
Assert.Equal(expected, b.Equals(a));
|
||||
Assert.Equal(expected, a.GetHashCode() == b.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMappedSubTextsNegativeTests()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => MappedSubText.GetMappedSubTextsAsync(null!, "Foo.cs", CancellationToken.None)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetMappedSubTextsTestData))]
|
||||
public async Task GetMappedSubTextsTests(string source, string? defaultMapPath, IEnumerable<MappedSubText> expected)
|
||||
{
|
||||
// Arrange
|
||||
using var workspace = new AdhocWorkspace();
|
||||
var doc = CreateDoc(workspace, source);
|
||||
|
||||
// Act
|
||||
var subTexts = await MappedSubText.GetMappedSubTextsAsync(doc, defaultMapPath, CancellationToken.None).ConfigureAwait(true);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(subTexts, expected.Select<MappedSubText, Action<MappedSubText>>(e => a => Assert.Equal(e, a)).ToArray());
|
||||
}
|
||||
|
||||
private static Document CreateDoc(AdhocWorkspace workspace, string source) =>
|
||||
workspace.AddProject("TestProject", "C#").AddDocument("TestDocument", SourceText.From(source));
|
||||
|
||||
public static IEnumerable<object[]> EqualsTestData =>
|
||||
new List<object[]>
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("a"), "Foo.txt", 83),
|
||||
new MappedSubText(SourceText.From("a"), "foo.txt", 83),
|
||||
true
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("a"), "Foo.txt", 83),
|
||||
new MappedSubText(SourceText.From("A"), "foo.txt", 83),
|
||||
false
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("a"), "foo.txt", 83),
|
||||
new MappedSubText(SourceText.From("a"), "foo.tx", 83),
|
||||
false
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("a"), "foo.txt", 83),
|
||||
new MappedSubText(SourceText.From("a"), "foo.txt", 84),
|
||||
false
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From(string.Empty), "Foo.txt", 83),
|
||||
new MappedSubText(SourceText.From("a"), "foo.txt", 83),
|
||||
false
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From(string.Empty), "Foo.txt", 0),
|
||||
new MappedSubText(SourceText.From(string.Empty), "foo.txt", 0),
|
||||
true
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("abc\ndef").GetSubText(4), string.Empty, 83),
|
||||
new MappedSubText(SourceText.From("def"), string.Empty, 83),
|
||||
true
|
||||
},
|
||||
};
|
||||
|
||||
public static IEnumerable<object?[]> GetMappedSubTextsTestData =>
|
||||
new List<object?[]>
|
||||
{
|
||||
new object?[]
|
||||
{
|
||||
string.Empty,
|
||||
null,
|
||||
Enumerable.Empty<MappedSubText>()
|
||||
},
|
||||
|
||||
new object?[]
|
||||
{
|
||||
string.Empty,
|
||||
"C:\\Foo.cs",
|
||||
new MappedSubText[]
|
||||
{
|
||||
new MappedSubText(SourceText.From(string.Empty), "C:\\Foo.cs", 0)
|
||||
}
|
||||
},
|
||||
|
||||
new object?[]
|
||||
{
|
||||
File.ReadAllText("MappedSubTextTestData.cs"),
|
||||
"Bar.cshtml",
|
||||
new MappedSubText[]
|
||||
{
|
||||
new MappedSubText(SourceText.From("// Auto-generated by Razor engine\r\n\r\nnamespace Razor\r\n{"), "Bar.cshtml", 0),
|
||||
new MappedSubText(SourceText.From(" using System;\r\n"), "test.cshtml", 1),
|
||||
new MappedSubText(SourceText.From("\r\n var foo = \"Hello World!\";\r\n"), "test.cshtml", 3),
|
||||
new MappedSubText(SourceText.From(" __PTagHelper.FooProp = 123;\r\n"), "test.cshtml", 7),
|
||||
new MappedSubText(SourceText.From(" WriteLiteral(foo);\r\n"), "test.cshtml", 7),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -7,11 +7,18 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="**/*.cshtml" />
|
||||
<Content Include="**/*.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Remove="InvalidViews\Invalid.cshtml" />
|
||||
<Content Include="NoViews\Dummy.txt">
|
||||
<Content Include="**/*.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="MappedSubTextTestData.cs" />
|
||||
<Content Include="MappedSubTextTestData.cs">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Autofac;
|
||||
using Autofac.Extras.Moq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public class RazorSourceUpdaterTests
|
||||
{
|
||||
[Fact]
|
||||
public void CtorNegativeTests()
|
||||
{
|
||||
using var mock = GetMock("RazorUpdaterStepViews/Test.csproj", Array.Empty<LocationLookup[]>());
|
||||
var analyzers = mock.Container.Resolve<IEnumerable<DiagnosticAnalyzer>>();
|
||||
var textMatcher = mock.Container.Resolve<ITextMatcher>();
|
||||
var codeFixProviders = mock.Container.Resolve<IEnumerable<CodeFixProvider>>();
|
||||
var textReplacer = mock.Mock<ITextReplacer>();
|
||||
var logger = mock.Mock<ILogger<RazorSourceUpdater>>();
|
||||
|
||||
Assert.Throws<ArgumentNullException>("analyzers", () => new RazorSourceUpdater(null!, codeFixProviders, textMatcher, textReplacer.Object, logger.Object));
|
||||
Assert.Throws<ArgumentNullException>("codeFixProviders", () => new RazorSourceUpdater(analyzers, null!, textMatcher, textReplacer.Object, logger.Object));
|
||||
Assert.Throws<ArgumentNullException>("textMatcher", () => new RazorSourceUpdater(analyzers, codeFixProviders, null!, textReplacer.Object, logger.Object));
|
||||
Assert.Throws<ArgumentNullException>("textReplacer", () => new RazorSourceUpdater(analyzers, codeFixProviders, textMatcher, null!, logger.Object));
|
||||
Assert.Throws<ArgumentNullException>("logger", () => new RazorSourceUpdater(analyzers, codeFixProviders, textMatcher, textReplacer.Object, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyTests()
|
||||
{
|
||||
using var mock = GetMock("RazorUpdaterStepViews/Test.csproj", Array.Empty<LocationLookup[]>());
|
||||
var updater = mock.Create<RazorSourceUpdater>();
|
||||
|
||||
Assert.Equal("Microsoft.DotNet.UpgradeAssistant.Steps.Razor.RazorSourceUpdater", updater.Id);
|
||||
Assert.Equal("Apply code fixes to Razor documents", updater.Title);
|
||||
Assert.Equal("Update code within Razor documents to fix diagnostics according to registered Roslyn analyzers and code fix providers", updater.Description);
|
||||
Assert.Equal(BuildBreakRisk.Medium, updater.Risk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsApplicableNegativeTests()
|
||||
{
|
||||
using var mock = GetMock(null, Array.Empty<LocationLookup[]>());
|
||||
var updater = mock.Create<RazorSourceUpdater>();
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>("context", () => updater.IsApplicableAsync(null!, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None)).ConfigureAwait(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => updater.IsApplicableAsync(mock.Mock<IUpgradeContext>().Object, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IsApplicableData))]
|
||||
public async Task IsApplicableTests(LocationLookup[][] diagnosticLocations, string[] expectedApplicableFilePaths)
|
||||
{
|
||||
// Arrange
|
||||
using var mock = GetMock("RazorUpdaterStepViews/Test.csproj", diagnosticLocations);
|
||||
var razorDocs = await GetRazorCodeDocumentsAsync(mock).ConfigureAwait(true);
|
||||
var context = mock.Mock<IUpgradeContext>();
|
||||
var updater = mock.Create<RazorSourceUpdater>();
|
||||
|
||||
// Act
|
||||
var result = (FileUpdaterResult)await updater.IsApplicableAsync(context.Object, ImmutableArray.CreateRange(razorDocs), CancellationToken.None).ConfigureAwait(true);
|
||||
var resultWithoutDocs = (FileUpdaterResult)await updater.IsApplicableAsync(context.Object, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None).ConfigureAwait(true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedApplicableFilePaths.Any(), result.Result);
|
||||
Assert.Collection(result.FilePaths.OrderBy(f => f), expectedApplicableFilePaths.OrderBy(f => f).Select<string, Action<string>>(expected => actual => Assert.Equal(expected, actual)).ToArray());
|
||||
Assert.False(resultWithoutDocs.Result);
|
||||
Assert.Empty(resultWithoutDocs.FilePaths);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> IsApplicableData =>
|
||||
new List<object[]>
|
||||
{
|
||||
// No diagnostcs reported
|
||||
new object[]
|
||||
{
|
||||
Array.Empty<LocationLookup[]>(),
|
||||
Array.Empty<string>(),
|
||||
},
|
||||
|
||||
// Diagnostics in non-Razor files
|
||||
new object[]
|
||||
{
|
||||
new[] { new[] { new LocationLookup("Foo.cs", null, 10, 15) } },
|
||||
Array.Empty<string>(),
|
||||
},
|
||||
|
||||
// Diagnostic in Razor file
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[] { new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[0]") },
|
||||
},
|
||||
new[] { GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml") },
|
||||
},
|
||||
|
||||
// Diagnostic mapped to shared Razor file
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[] { new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "using Microsoft.AspNetCore.Mvc;") },
|
||||
},
|
||||
new[] { GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml") },
|
||||
},
|
||||
|
||||
// Diagnostic in unmapped portions of generated files
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "assembly: global::Microsoft.AspNetCore"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "WriteLiteral(\" <div>\\r\\n <p>\")"),
|
||||
},
|
||||
},
|
||||
new[]
|
||||
{
|
||||
GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs"),
|
||||
},
|
||||
},
|
||||
|
||||
// Diagnostics in multiple files (from multiple analyzers)
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "using Microsoft.AspNetCore.Mvc;"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[0]"),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[1]"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\Simple.cshtml.cs", "using Microsoft.AspNetCore.Mvc;"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\Simple.cshtml.cs", "DateTime.Now.ToString()"),
|
||||
}
|
||||
},
|
||||
new[]
|
||||
{
|
||||
GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml"),
|
||||
GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"),
|
||||
GetFullPath("RazorUpdaterStepViews\\TestViews\\Simple.cshtml"),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyNegativeTests()
|
||||
{
|
||||
using var mock = GetMock(null, Array.Empty<LocationLookup[]>());
|
||||
var updater = mock.Create<RazorSourceUpdater>();
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>("context", () => updater.ApplyAsync(null!, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None)).ConfigureAwait(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => updater.ApplyAsync(mock.Mock<IUpgradeContext>().Object, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None)).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ApplyData))]
|
||||
public async Task ApplyTests(LocationLookup[][] diagnosticLocations, string[] expectedUpdatedFiles, TextReplacement[] expectedReplacements)
|
||||
{
|
||||
// Arrange
|
||||
using var mock = GetMock("RazorUpdaterStepViews/Test.csproj", diagnosticLocations);
|
||||
var razorDocs = await GetRazorCodeDocumentsAsync(mock).ConfigureAwait(true);
|
||||
var context = mock.Mock<IUpgradeContext>();
|
||||
var updater = mock.Create<RazorSourceUpdater>();
|
||||
var replacements = new List<TextReplacement>();
|
||||
var textReplacer = mock.Mock<ITextReplacer>();
|
||||
textReplacer.Setup(r => r.ApplyTextReplacements(It.IsAny<IEnumerable<TextReplacement>>()))
|
||||
.Callback<IEnumerable<TextReplacement>>(newReplacements => replacements.AddRange(newReplacements));
|
||||
|
||||
// Act
|
||||
var result = (FileUpdaterResult)await updater.ApplyAsync(context.Object, ImmutableArray.CreateRange(razorDocs), CancellationToken.None).ConfigureAwait(true);
|
||||
var resultWithoutDocs = (FileUpdaterResult)await updater.ApplyAsync(context.Object, ImmutableArray.Create<RazorCodeDocument>(), CancellationToken.None).ConfigureAwait(true);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Result);
|
||||
Assert.Collection(result.FilePaths.OrderBy(f => f), expectedUpdatedFiles.OrderBy(f => f).Select<string, Action<string>>(expected => actual => Assert.Equal(expected, actual)).ToArray());
|
||||
Assert.True(resultWithoutDocs.Result);
|
||||
Assert.Empty(resultWithoutDocs.FilePaths);
|
||||
Assert.Collection(replacements, expectedReplacements.Select<TextReplacement, Action<TextReplacement>>(e => a =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.OriginalText.ToString()) && string.IsNullOrEmpty(e.NewText.ToString()))
|
||||
{
|
||||
Assert.Equal(e.FilePath, a.FilePath);
|
||||
Assert.Equal(e.StartingLine, a.StartingLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(e, a);
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ApplyData =>
|
||||
new List<object[]>
|
||||
{
|
||||
// No diagnostcs reported
|
||||
new object[]
|
||||
{
|
||||
Array.Empty<LocationLookup[]>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<TextReplacement>(),
|
||||
},
|
||||
|
||||
// Diagnostics in non-Razor files
|
||||
new object[]
|
||||
{
|
||||
new[] { new[] { new LocationLookup("Foo.cs", null, 10, 15) } },
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<TextReplacement>(),
|
||||
},
|
||||
|
||||
// Diagnostic in Razor file
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[] { new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[0]") },
|
||||
},
|
||||
new[] { GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml") },
|
||||
new[] { new TextReplacement(" Write(Model[0]);\r\n", " Write(Model[0] /* Test! */);\r\n", GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"), 6) }
|
||||
},
|
||||
|
||||
// Diagnostic mapped to shared Razor file
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[] { new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "using Microsoft.AspNetCore.Mvc;") },
|
||||
},
|
||||
new[] { GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml") },
|
||||
new[] { new TextReplacement("using Microsoft.AspNetCore.Mvc;\r\n", "using Microsoft.AspNetCore.Mvc; /* Test! */\r\n", GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml"), 1) }
|
||||
},
|
||||
|
||||
// Diagnostic in unmapped portions of generated files
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "assembly: global::Microsoft.AspNetCore"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "WriteLiteral(\" <div>\\r\\n <p>\")"),
|
||||
},
|
||||
},
|
||||
new[] { GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml") },
|
||||
new[]
|
||||
{
|
||||
// The first one *does* generate a replacement because it represents text being prepended to the beginning of the source file
|
||||
// Don't check the actual text, though, since it will include file path-specific values that will change
|
||||
new TextReplacement(string.Empty, string.Empty, GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"), 0),
|
||||
}
|
||||
},
|
||||
|
||||
// Diagnostics in multiple files (from multiple analyzers)
|
||||
new object[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "using Microsoft.AspNetCore.Mvc;"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[0]"),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\View.cshtml.cs", "Model[1]"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\Simple.cshtml.cs", "using Microsoft.AspNetCore.Mvc;"),
|
||||
new LocationLookup("RazorUpdaterStepViews\\TestViews\\Simple.cshtml.cs", "DateTime.Now.ToString()"),
|
||||
}
|
||||
},
|
||||
new[]
|
||||
{
|
||||
GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml"),
|
||||
GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"),
|
||||
GetFullPath("RazorUpdaterStepViews\\TestViews\\Simple.cshtml"),
|
||||
},
|
||||
new[]
|
||||
{
|
||||
new TextReplacement("using Microsoft.AspNetCore.Mvc;\r\n", "using Microsoft.AspNetCore.Mvc; /* Test! */\r\n", GetFullPath("RazorUpdaterStepViews\\_ViewImports.cshtml"), 1),
|
||||
new TextReplacement(" Write(DateTime.Now.ToString());\r\n", " Write(DateTime.Now.ToString() /* Test! */);\r\n", GetFullPath("RazorUpdaterStepViews\\TestViews\\Simple.cshtml"), 1),
|
||||
new TextReplacement(" Write(Model[0]);\r\n", " Write(Model[0] /* Test! */);\r\n", GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"), 6),
|
||||
new TextReplacement(" Write(Model[1]);\r\n", " Write(Model[1] /* Test! */);\r\n", GetFullPath("RazorUpdaterStepViews\\TestViews\\View.cshtml"), 18),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static AutoMock GetMock(string? projectPath, LocationLookup[][] diagnosticLocations)
|
||||
{
|
||||
var mock = AutoMock.GetLoose(builder =>
|
||||
{
|
||||
builder.RegisterType<DefaultTextMatcher>().As<ITextMatcher>();
|
||||
var analyzers = new List<DiagnosticAnalyzer>();
|
||||
var codeFixProviders = new List<CodeFixProvider>();
|
||||
|
||||
if (diagnosticLocations.Any())
|
||||
{
|
||||
for (var i = 0; i < diagnosticLocations.Length; i++)
|
||||
{
|
||||
var analyzer = new Mock<DiagnosticAnalyzer>();
|
||||
var descriptor = new DiagnosticDescriptor($"Test{i}", $"Test diagnostic {i}", $"Test message {i}", "Test", DiagnosticSeverity.Warning, true);
|
||||
var locations = diagnosticLocations[i];
|
||||
analyzer.Setup(a => a.SupportedDiagnostics).Returns(ImmutableArray.Create(descriptor));
|
||||
analyzer.Setup(a => a.Initialize(It.IsAny<AnalysisContext>())).Callback<AnalysisContext>(context => context.RegisterSyntaxTreeAction(x =>
|
||||
{
|
||||
foreach (var lookup in locations.Where(l => GetFullPath(l.Path).Equals(x.Tree.FilePath, StringComparison.Ordinal)))
|
||||
{
|
||||
var start = lookup.StartOffset;
|
||||
var end = lookup.EndOffset;
|
||||
|
||||
if (lookup.Keyword is not null)
|
||||
{
|
||||
var index = x.Tree.GetText().ToString().IndexOf(lookup.Keyword, StringComparison.Ordinal);
|
||||
(start, end) = (index, index + lookup.Keyword.Length);
|
||||
}
|
||||
|
||||
// If the 'test' trivia hasn't been added by the code fix yet, report a diagnostic
|
||||
var line = x.Tree.GetText().Lines.GetLineFromPosition(start);
|
||||
var lineText = x.Tree.GetText().GetSubText(line.Span).ToString();
|
||||
if (!lineText.Contains("Test!", StringComparison.Ordinal))
|
||||
{
|
||||
var location = Location.Create(x.Tree, TextSpan.FromBounds(start, end));
|
||||
var diagnostic = Diagnostic.Create(descriptor, location);
|
||||
x.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}));
|
||||
builder.RegisterMock(analyzer);
|
||||
|
||||
builder.RegisterInstance<CodeFixProvider>(new TestCodeFixProvider(new[] { descriptor.Id }));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.RegisterInstance(Enumerable.Empty<DiagnosticAnalyzer>());
|
||||
builder.RegisterInstance(Enumerable.Empty<CodeFixProvider>());
|
||||
}
|
||||
});
|
||||
|
||||
var projectFile = mock.Mock<IProjectFile>();
|
||||
var project = projectPath is not null ? mock.Mock<IProject>() : null;
|
||||
project?.Setup(p => p.FileInfo).Returns(new FileInfo(projectPath!));
|
||||
project?.Setup(p => p.GetFile()).Returns(projectFile.Object);
|
||||
project?.Setup(p => p.GetRoslynProject()).Returns(() =>
|
||||
{
|
||||
var ws = new AdhocWorkspace();
|
||||
var name = Path.GetFileNameWithoutExtension(projectPath)!;
|
||||
return ws.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Default, name, name, "C#", filePath: projectPath));
|
||||
});
|
||||
var context = mock.Mock<IUpgradeContext>();
|
||||
context.Setup(c => c.CurrentProject).Returns(project?.Object);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<RazorCodeDocument>> GetRazorCodeDocumentsAsync(AutoMock mock)
|
||||
{
|
||||
var updaterStep = new RazorUpdaterStep(Enumerable.Empty<IUpdater<RazorCodeDocument>>(), mock.Mock<ILogger<RazorUpdaterStep>>().Object);
|
||||
await updaterStep.InitializeAsync(mock.Mock<IUpgradeContext>().Object, CancellationToken.None).ConfigureAwait(true);
|
||||
return updaterStep.RazorDocuments;
|
||||
}
|
||||
|
||||
private static string GetFullPath(string path) =>
|
||||
Path.IsPathFullyQualified(path)
|
||||
? path
|
||||
: Path.Combine(AppContext.BaseDirectory, path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Autofac.Extras.Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public sealed class RazorTextReplacerTests : IDisposable
|
||||
{
|
||||
private static readonly string WorkingDir = Path.Combine(Path.GetTempPath(), "RazorTextReplacerTestFiles");
|
||||
|
||||
public RazorTextReplacerTests()
|
||||
{
|
||||
if (Directory.Exists(WorkingDir))
|
||||
{
|
||||
Directory.Delete(WorkingDir, true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(WorkingDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(WorkingDir))
|
||||
{
|
||||
Directory.Delete(WorkingDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CtorNegativeTests()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>("logger", () => new RazorTextReplacer(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyTextReplacementsNegativeTests()
|
||||
{
|
||||
using var mock = AutoMock.GetLoose();
|
||||
var replacer = mock.Create<RazorTextReplacer>();
|
||||
|
||||
Assert.Throws<ArgumentNullException>("replacements", () => replacer.ApplyTextReplacements(null!));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ApplyReplacementsData))]
|
||||
public void ApplyTextReplacementsPositiveTests(string testCaseId, IEnumerable<TextReplacement> replacements)
|
||||
{
|
||||
// Arrange
|
||||
replacements = StageInputFiles(replacements);
|
||||
var inputFiles = replacements.Select(r => r.FilePath).Distinct().OrderBy(p => p);
|
||||
var expectedPostReplacementFiles = Directory.GetFiles(Path.Combine("TestViewsAfterReplacement", testCaseId), "*.cshtml").OrderBy(p => p);
|
||||
using var mock = AutoMock.GetLoose();
|
||||
var replacer = mock.Create<RazorTextReplacer>();
|
||||
|
||||
// Act
|
||||
replacer.ApplyTextReplacements(replacements);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(inputFiles, expectedPostReplacementFiles.Select<string, Action<string>>(e => a =>
|
||||
{
|
||||
Assert.Equal(Path.GetFileName(e), Path.GetFileName(a));
|
||||
Assert.Equal(File.ReadAllText(e), File.ReadAllText(a));
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ApplyReplacementsData =>
|
||||
new List<object[]>
|
||||
{
|
||||
// No replacements
|
||||
new object[]
|
||||
{
|
||||
"NoReplacements",
|
||||
Enumerable.Empty<TextReplacement>()
|
||||
},
|
||||
|
||||
// Vanilla replacements
|
||||
new object[]
|
||||
{
|
||||
"VanillaReplacements",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("ToString()", "ToAnotherString()", GetPath("Simple.cshtml"), 1),
|
||||
new TextReplacement("Model[1]", "Model[2]", GetPath("View.cshtml"), 18),
|
||||
new TextReplacement("if(Model != null && Model.Length > 1)", "if(Model is not null)", GetPath("View.cshtml"), 15)
|
||||
}
|
||||
},
|
||||
|
||||
// Multi-line replacement
|
||||
new object[]
|
||||
{
|
||||
"MultilineReplacement",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement(
|
||||
"{\r\n <div>\r\n <p>@Model[1]</p>\r\n </div>\r\n}\r\n",
|
||||
"<h1>\r\n Hi!\r\n</h1>",
|
||||
GetPath("View.cshtml"),
|
||||
16)
|
||||
}
|
||||
},
|
||||
|
||||
// Inapplicable replacement
|
||||
new object[]
|
||||
{
|
||||
"InapplicableReplacement",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("DateTime", "DateTimeOffset", GetPath("Simple.cshtml"), 2),
|
||||
new TextReplacement("<div>\r\n ", "<div>\r\n", GetPath("View.cshtml"), 9),
|
||||
}
|
||||
},
|
||||
|
||||
// Adding code to start-of-file
|
||||
new object[]
|
||||
{
|
||||
"NewTextAtStart",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("Something inapplicable", "using Foo;\r\nSomething inapplicable", GetPath("Simple.cshtml"), 0),
|
||||
new TextReplacement(string.Empty, "Test", GetPath("View.cshtml"), 0),
|
||||
}
|
||||
},
|
||||
|
||||
// Remove code block
|
||||
new object[]
|
||||
{
|
||||
"RemoveCodeBlock",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement("DateTime.Now.ToString();", string.Empty, GetPath("Simple.cshtml"), 1),
|
||||
new TextReplacement("using Foo;", string.Empty, GetPath("View2.cshtml"), 1),
|
||||
new TextReplacement("Model[0]", string.Empty, GetPath("View2.cshtml"), 7),
|
||||
new TextReplacement("\r\n var x = 0;\r\n", string.Empty, GetPath("View2.cshtml"), 23),
|
||||
}
|
||||
},
|
||||
|
||||
// Remove partial code block
|
||||
new object[]
|
||||
{
|
||||
"RemovePartialCodeBlock",
|
||||
new TextReplacement[]
|
||||
{
|
||||
new TextReplacement(".ToString();", ";", GetPath("Simple.cshtml"), 1),
|
||||
new TextReplacement(" Foo", string.Empty, GetPath("View2.cshtml"), 1),
|
||||
new TextReplacement("Model[0];", "Model;", GetPath("View2.cshtml"), 7),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static string GetPath(string fileName) => Path.Combine(AppContext.BaseDirectory, "TestViews", fileName);
|
||||
|
||||
private static IEnumerable<TextReplacement> StageInputFiles(IEnumerable<TextReplacement> replacements)
|
||||
{
|
||||
var inputFiles = replacements.Select(r => r.FilePath).Distinct();
|
||||
|
||||
foreach (var inputFile in inputFiles)
|
||||
{
|
||||
File.Copy(inputFile, Path.Combine(WorkingDir, Path.GetFileName(inputFile)), true);
|
||||
}
|
||||
|
||||
return replacements.Select(r => new TextReplacement(r.OriginalText, r.NewText, Path.Combine(WorkingDir, Path.GetFileName(r.FilePath)), r.StartingLine));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,11 +60,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("TestViews/Test.csproj", 0, 2, true, true)] // Vanilla positive case
|
||||
[InlineData("TestViews/Test.csproj", 1, 1, false, false)] // Not applicable if no project is loaded
|
||||
[InlineData("TestViews/Test.csproj", 0, 0, true, false)] // Not applicable if there are no updaters
|
||||
[InlineData("NoViews/Test.csproj", 0, 1, true, false)] // Not applicable if there are no Razor pages
|
||||
[InlineData("Test.csproj", 1, 0, true, true)] // Applicable even with only complete updaters (updater status is not checked)
|
||||
[InlineData("RazorUpdaterStepViews/TestViews/Test.csproj", 0, 2, true, true)] // Vanilla positive case
|
||||
[InlineData("RazorUpdaterStepViews/TestViews/Test.csproj", 1, 1, false, false)] // Not applicable if no project is loaded
|
||||
[InlineData("RazorUpdaterStepViews/TestViews/Test.csproj", 0, 0, true, false)] // Not applicable if there are no updaters
|
||||
[InlineData("RazorUpdaterStepViews/NoViews/Test.csproj", 0, 1, true, false)] // Not applicable if there are no Razor pages
|
||||
[InlineData("RazorUpdaterStepViews/Test.csproj", 1, 0, true, true)] // Applicable even with only complete updaters (updater status is not checked)
|
||||
public async Task IsApplicableTests(string projectPath, int completeUpdaterCount, int incompleteUpdaterCount, bool projectLoaded, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
|
@ -88,11 +88,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Test.csproj", 0, 2, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Vanilla positive case
|
||||
[InlineData("TestViews/Test.csproj", 2, 2, false, new[] { "Simple.cshtml", "View.cshtml" })] // Mixed complete and incomplete updaters and no _ViewImports
|
||||
[InlineData("Test.csproj", 2, 0, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Test with no incomplete updaters
|
||||
[InlineData("NoViews/Test.csproj", 2, 2, true, new string[0])] // Test with no Razor documents
|
||||
[InlineData("Test.csproj", 0, 0, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // No sub-steps
|
||||
[InlineData("RazorUpdaterStepViews/Test.csproj", 0, 2, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Vanilla positive case
|
||||
[InlineData("RazorUpdaterStepViews/TestViews/Test.csproj", 2, 2, false, new[] { "Simple.cshtml", "View.cshtml", })] // Mixed complete and incomplete updaters and no _ViewImports
|
||||
[InlineData("RazorUpdaterStepViews/Test.csproj", 2, 0, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Test with no incomplete updaters
|
||||
[InlineData("RazorUpdaterStepViews/NoViews/Test.csproj", 2, 2, true, new string[0])] // Test with no Razor documents
|
||||
[InlineData("RazorUpdaterStepViews/Test.csproj", 0, 0, true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // No sub-steps
|
||||
public async Task InitializeTests(string projectPath, int completeUpdaterCount, int incompleteUpdaterCount, bool expectImports, string[] expectedFiles)
|
||||
{
|
||||
// Arrange
|
||||
|
@ -150,9 +150,9 @@ namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Test.csproj", true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Vanilla positive case
|
||||
[InlineData("TestViews/Test.csproj", false, new[] { "Simple.cshtml", "View.cshtml" })] // No _ViewImports
|
||||
[InlineData("NoViews/Test.csproj", false, new string[0])] // No Razor documents
|
||||
[InlineData("RazorUpdaterStepViews/Test.csproj", true, new[] { "_ViewImports.cshtml", "Invalid.cshtml", "Simple.cshtml", "View.cshtml" })] // Vanilla positive case
|
||||
[InlineData("RazorUpdaterStepViews/TestViews/Test.csproj", false, new[] { "Simple.cshtml", "View.cshtml" })] // No _ViewImports
|
||||
[InlineData("RazorUpdaterStepViews/NoViews/Test.csproj", false, new string[0])] // No Razor documents
|
||||
|
||||
public async Task ProcessRazorDocumentsTests(string projectPath, bool expectImports, string[] expectedFiles)
|
||||
{
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
It is @DateTime.Now.ToString()
|
|
@ -0,0 +1,20 @@
|
|||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[0]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Editing;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Steps.Razor.Tests
|
||||
{
|
||||
public class TestCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private readonly ImmutableArray<string> _diagnosticIds;
|
||||
|
||||
public TestCodeFixProvider(IEnumerable<string> diagnosticIds)
|
||||
{
|
||||
_diagnosticIds = ImmutableArray.CreateRange(diagnosticIds ?? throw new ArgumentNullException(nameof(diagnosticIds)));
|
||||
}
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => _diagnosticIds;
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(true);
|
||||
|
||||
if (root is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var node = root.FindNode(context.Span);
|
||||
|
||||
if (node is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var diagnostic in context.Diagnostics)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
$"Fix {diagnostic.Id}",
|
||||
ct => ReplaceNodeAsync(context.Document, node, ct),
|
||||
$"Fix {diagnostic.Id}"),
|
||||
context.Diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> ReplaceNodeAsync(Document document, SyntaxNode node, CancellationToken ct)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
|
||||
editor.ReplaceNode(node, node.WithTrailingTrivia(SyntaxFactory.ParseTrailingTrivia(" /* Test! */").AddRange(node.GetTrailingTrivia())));
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
@using Foo
|
||||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@(Model[0])</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@{
|
||||
var x = 0;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
It is @DateTime.Now.ToString()
|
|
@ -0,0 +1,20 @@
|
|||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[0]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[0]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
<h1>
|
||||
Hi!
|
||||
</h1>
|
|
@ -0,0 +1,2 @@
|
|||
@using Foo
|
||||
It is @DateTime.Now.ToString()
|
|
@ -0,0 +1,20 @@
|
|||
@{ Test }@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[0]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Place-holder file so that a test folder without cshtml files will be available
|
|
@ -0,0 +1 @@
|
|||
It is
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p></p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@{
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
It is @DateTime.Now
|
|
@ -0,0 +1,25 @@
|
|||
@using
|
||||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@(Model)</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model != null && Model.Length > 1)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[1]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@{
|
||||
var x = 0;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
It is @DateTime.Now.ToAnotherString()
|
|
@ -0,0 +1,20 @@
|
|||
@model string[]
|
||||
|
||||
@if(Model != null && Model.Length > 0)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[0]</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Hello world!</h1>
|
||||
<p>This is the body</p>
|
||||
</div>
|
||||
|
||||
@if(Model is not null)
|
||||
{
|
||||
<div>
|
||||
<p>@Model[2]</p>
|
||||
</div>
|
||||
}
|
Загрузка…
Ссылка в новой задаче