/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/TestUtilities/SignatureHelp/AbstractSignatureHelpProviderTests.cs
486 строк
22 KB
Jason Malinowski
Remove AssertEx.Fail
21 апр 2026, 23:28
21 апр 2026, 23:28
7b44707
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp.Presentation; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SignatureHelp; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Editor.UnitTests.SignatureHelp; [UseExportProvider] public abstract class AbstractSignatureHelpProviderTests<TWorkspaceFixture> : TestBase where TWorkspaceFixture : TestWorkspaceFixture, new() { private readonly TestFixtureHelper<TWorkspaceFixture> _fixtureHelper = new(); internal abstract Type GetSignatureHelpProviderType(); private protected ReferenceCountedDisposable<TWorkspaceFixture> GetOrCreateWorkspaceFixture() => _fixtureHelper.GetOrCreateFixture(); /// <summary> /// Verifies that sighelp comes up at the indicated location in markup ($$), with the indicated span [| ... |]. /// </summary> /// <param name="markup">Input markup with $$ denoting the cursor position, and [| ... |] /// denoting the expected sighelp span</param> /// <param name="expectedOrderedItemsOrNull">The exact expected sighelp items list. If null, this part of the test is ignored.</param> /// <param name="usePreviousCharAsTrigger">If true, uses the last character before $$ to trigger sighelp. /// If false, invokes sighelp explicitly at the cursor location.</param> /// <param name="sourceCodeKind">The sourcecodekind to run this test on. If null, runs on both regular and script sources.</param> protected virtual async Task TestAsync( string markup, IEnumerable<SignatureHelpTestItem> expectedOrderedItemsOrNull = null, bool usePreviousCharAsTrigger = false, SourceCodeKind? sourceCodeKind = null, bool experimental = false) { using var workspaceFixture = GetOrCreateWorkspaceFixture(); if (sourceCodeKind.HasValue) { await TestSignatureHelpWorkerAsync(markup, sourceCodeKind.Value, experimental, expectedOrderedItemsOrNull, usePreviousCharAsTrigger); } else { await TestSignatureHelpWorkerAsync(markup, SourceCodeKind.Regular, experimental, expectedOrderedItemsOrNull, usePreviousCharAsTrigger); await TestSignatureHelpWorkerAsync(markup, SourceCodeKind.Script, experimental, expectedOrderedItemsOrNull, usePreviousCharAsTrigger); } } private async Task TestSignatureHelpWorkerAsync( string markupWithPositionAndOptSpan, SourceCodeKind sourceCodeKind, bool experimental, IEnumerable<SignatureHelpTestItem> expectedOrderedItemsOrNull = null, bool usePreviousCharAsTrigger = false) { using var workspaceFixture = GetOrCreateWorkspaceFixture(); var options = new MemberDisplayOptions(); markupWithPositionAndOptSpan = markupWithPositionAndOptSpan.NormalizeLineEndings(); var workspace = workspaceFixture.Target.GetWorkspace(markupWithPositionAndOptSpan); var code = workspaceFixture.Target.Code; var cursorPosition = workspaceFixture.Target.Position; var textSpan = workspaceFixture.Target.Spans.FirstOrNull(); var parseOptions = CreateExperimentalParseOptions(); // regular var document1 = workspaceFixture.Target.UpdateDocument(code, sourceCodeKind); if (experimental) { document1 = document1.Project.WithParseOptions(parseOptions).GetDocument(document1.Id); } await TestSignatureHelpWorkerSharedAsync(workspaceFixture.Target.GetWorkspace(), code, cursorPosition, document1, options, textSpan, expectedOrderedItemsOrNull, usePreviousCharAsTrigger); // speculative semantic model if (await CanUseSpeculativeSemanticModelAsync(document1, cursorPosition)) { var document2 = workspaceFixture.Target.UpdateDocument(code, sourceCodeKind, cleanBeforeUpdate: false); if (experimental) { document2 = document2.Project.WithParseOptions(parseOptions).GetDocument(document2.Id); } await TestSignatureHelpWorkerSharedAsync(workspaceFixture.Target.GetWorkspace(), code, cursorPosition, document2, options, textSpan, expectedOrderedItemsOrNull, usePreviousCharAsTrigger); } } protected abstract ParseOptions CreateExperimentalParseOptions(); private static async Task<bool> CanUseSpeculativeSemanticModelAsync(Document document, int position) { var service = document.GetLanguageService<ISyntaxFactsService>(); var node = (await document.GetSyntaxRootAsync()).FindToken(position).Parent; return !service.GetMemberBodySpanForSpeculativeBinding(node).IsEmpty; } protected void VerifyTriggerCharacters(char[] expectedTriggerCharacters, char[] unexpectedTriggerCharacters) { using var workspaceFixture = GetOrCreateWorkspaceFixture(); var signatureHelpProviderType = GetSignatureHelpProviderType(); var signatureHelpProvider = workspaceFixture.Target.GetWorkspace().ExportProvider.GetExportedValues<ISignatureHelpProvider>().Single(provider => provider.GetType() == signatureHelpProviderType); foreach (var expectedTriggerCharacter in expectedTriggerCharacters) { Assert.True(signatureHelpProvider.TriggerCharacters.Contains(expectedTriggerCharacter), "Expected '" + expectedTriggerCharacter + "' to be a trigger character"); } foreach (var unexpectedTriggerCharacter in unexpectedTriggerCharacters) { Assert.False(signatureHelpProvider.TriggerCharacters.Contains(unexpectedTriggerCharacter), "Expected '" + unexpectedTriggerCharacter + "' to NOT be a trigger character"); } } protected virtual async Task VerifyCurrentParameterNameAsync(string markup, string expectedParameterName, SourceCodeKind? sourceCodeKind = null) { using var workspaceFixture = GetOrCreateWorkspaceFixture(); if (sourceCodeKind.HasValue) { await VerifyCurrentParameterNameWorkerAsync(markup, expectedParameterName, sourceCodeKind.Value); } else { await VerifyCurrentParameterNameWorkerAsync(markup, expectedParameterName, SourceCodeKind.Regular); await VerifyCurrentParameterNameWorkerAsync(markup, expectedParameterName, SourceCodeKind.Script); } } private static async Task<SignatureHelpState?> GetArgumentStateAsync(int cursorPosition, Document document, ISignatureHelpProvider signatureHelpProvider, SignatureHelpTriggerInfo triggerInfo, MemberDisplayOptions options) { var items = await signatureHelpProvider.GetItemsAsync(document, cursorPosition, triggerInfo, options, CancellationToken.None); return items == null ? null : new SignatureHelpState(items.SemanticParameterIndex, items.SyntacticArgumentCount, items.ArgumentName, ArgumentNames: default); } private async Task VerifyCurrentParameterNameWorkerAsync(string markup, string expectedParameterName, SourceCodeKind sourceCodeKind) { using var workspaceFixture = GetOrCreateWorkspaceFixture(); MarkupTestFile.GetPosition(markup.NormalizeLineEndings(), out var code, out int cursorPosition); var document = workspaceFixture.Target.UpdateDocument(code, sourceCodeKind); var signatureHelpProviderType = GetSignatureHelpProviderType(); var signatureHelpProvider = workspaceFixture.Target.GetWorkspace().ExportProvider.GetExportedValues<ISignatureHelpProvider>().Single(provider => provider.GetType() == signatureHelpProviderType); var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.InvokeSignatureHelpCommand); var options = new MemberDisplayOptions(); _ = await signatureHelpProvider.GetItemsAsync(document, cursorPosition, triggerInfo, options, CancellationToken.None); Assert.Equal(expectedParameterName, (await GetArgumentStateAsync(cursorPosition, document, signatureHelpProvider, triggerInfo, options)).Value.ArgumentName); } private static void CompareAndAssertCollectionsAndCurrentParameter( IEnumerable<SignatureHelpTestItem> expectedTestItems, SignatureHelpItems actualSignatureHelpItems) { Assert.True(expectedTestItems.Count() == actualSignatureHelpItems.Items.Count, $"Expected {expectedTestItems.Count()} items, but got {actualSignatureHelpItems.Items.Count}"); for (var i = 0; i < expectedTestItems.Count(); i++) { CompareSigHelpItemsAndCurrentPosition( actualSignatureHelpItems, actualSignatureHelpItems.Items.ElementAt(i), expectedTestItems.ElementAt(i)); } } private static void CompareSigHelpItemsAndCurrentPosition( SignatureHelpItems items, SignatureHelpItem actualSignatureHelpItem, SignatureHelpTestItem expectedTestItem) { var currentParameterIndex = -1; if (expectedTestItem.CurrentParameterIndex != null) { if (expectedTestItem.CurrentParameterIndex.Value >= 0 && expectedTestItem.CurrentParameterIndex.Value < actualSignatureHelpItem.Parameters.Length) { currentParameterIndex = expectedTestItem.CurrentParameterIndex.Value; } } var signature = new Signature(applicableToSpan: null, signatureHelpItem: actualSignatureHelpItem, selectedParameterIndex: currentParameterIndex); // We're a match if the signature matches... // We're now combining the signature and documentation to make classification work. if (!string.IsNullOrEmpty(expectedTestItem.MethodDocumentation)) { Assert.Equal(expectedTestItem.Signature + "\r\n" + expectedTestItem.MethodDocumentation, signature.Content); } else { AssertEx.Equal(expectedTestItem.Signature, signature.Content); } if (expectedTestItem.PrettyPrintedSignature != null) { Assert.Equal(expectedTestItem.PrettyPrintedSignature, signature.PrettyPrintedContent); } if (expectedTestItem.MethodDocumentation != null) { Assert.Equal(expectedTestItem.MethodDocumentation, actualSignatureHelpItem.DocumentationFactory(CancellationToken.None).GetFullText()); } if (expectedTestItem.ParameterDocumentation != null) { Assert.Equal(expectedTestItem.ParameterDocumentation, signature.CurrentParameter.Documentation); } if (expectedTestItem.CurrentParameterIndex != null) { Assert.True(expectedTestItem.CurrentParameterIndex == items.SemanticParameterIndex, $"The current parameter is {items.SemanticParameterIndex}, but we expected {expectedTestItem.CurrentParameterIndex}"); } if (expectedTestItem.Description != null) { Assert.Equal(expectedTestItem.Description, ToString(actualSignatureHelpItem.DescriptionParts)); } // Always get and realise the classified spans, even if no expected spans are passed in, to at least validate that // exceptions aren't thrown var classifiedSpans = actualSignatureHelpItem.DocumentationFactory(CancellationToken.None).ToClassifiedSpans().ToList(); if (expectedTestItem.ClassificationTypeNames is { } classificationTypeNames) { Assert.Equal(string.Join(", ", classificationTypeNames), string.Join(", ", classifiedSpans.Select(s => s.ClassificationType))); } } private static string ToString(IEnumerable<TaggedText> list) => string.Concat(list.Select(i => i.ToString())); protected async Task TestSignatureHelpInEditorBrowsableContextsAsync( string markup, string referencedCode, IEnumerable<SignatureHelpTestItem> expectedOrderedItemsMetadataReference, IEnumerable<SignatureHelpTestItem> expectedOrderedItemsSameSolution, string sourceLanguage, string referencedLanguage, bool hideAdvancedMembers = false) { if (expectedOrderedItemsMetadataReference == null || expectedOrderedItemsSameSolution == null) { Assert.Fail("Expected signature help items must be provided for EditorBrowsable tests. If there are no expected items, provide an empty IEnumerable rather than null."); } await TestSignatureHelpWithMetadataReferenceHelperAsync(markup, referencedCode, expectedOrderedItemsMetadataReference, sourceLanguage, referencedLanguage, hideAdvancedMembers); await TestSignatureHelpWithProjectReferenceHelperAsync(markup, referencedCode, expectedOrderedItemsSameSolution, sourceLanguage, referencedLanguage, hideAdvancedMembers); // Multi-language projects are not supported. if (sourceLanguage == referencedLanguage) { await TestSignatureHelpInSameProjectHelperAsync(markup, referencedCode, expectedOrderedItemsSameSolution, sourceLanguage, hideAdvancedMembers); } } public Task TestSignatureHelpWithMetadataReferenceHelperAsync(string sourceCode, string referencedCode, IEnumerable<SignatureHelpTestItem> expectedOrderedItems, string sourceLanguage, string referencedLanguage, bool hideAdvancedMembers) { var xmlString = string.Format(""" <Workspace> <Project Language="{0}" CommonReferences="true"> <Document FilePath="SourceDocument"> {1} </Document> <MetadataReferenceFromSource Language="{2}" CommonReferences="true"> <Document FilePath="ReferencedDocument"> {3} </Document> </MetadataReferenceFromSource> </Project> </Workspace> """, sourceLanguage, SecurityElement.Escape(sourceCode), referencedLanguage, SecurityElement.Escape(referencedCode)); return VerifyItemWithReferenceWorkerAsync(xmlString, expectedOrderedItems, hideAdvancedMembers); } public async Task TestSignatureHelpWithProjectReferenceHelperAsync(string sourceCode, string referencedCode, IEnumerable<SignatureHelpTestItem> expectedOrderedItems, string sourceLanguage, string referencedLanguage, bool hideAdvancedMembers) { var xmlString = string.Format(""" <Workspace> <Project Language="{0}" CommonReferences="true"> <ProjectReference>ReferencedProject</ProjectReference> <Document FilePath="SourceDocument"> {1} </Document> </Project> <Project Language="{2}" CommonReferences="true" AssemblyName="ReferencedProject"> <Document FilePath="ReferencedDocument"> {3} </Document> </Project> </Workspace> """, sourceLanguage, SecurityElement.Escape(sourceCode), referencedLanguage, SecurityElement.Escape(referencedCode)); await VerifyItemWithReferenceWorkerAsync(xmlString, expectedOrderedItems, hideAdvancedMembers); } private async Task TestSignatureHelpInSameProjectHelperAsync(string sourceCode, string referencedCode, IEnumerable<SignatureHelpTestItem> expectedOrderedItems, string sourceLanguage, bool hideAdvancedMembers) { var xmlString = string.Format(""" <Workspace> <Project Language="{0}" CommonReferences="true"> <Document FilePath="SourceDocument"> {1} </Document> <Document FilePath="ReferencedDocument"> {2} </Document> </Project> </Workspace> """, sourceLanguage, SecurityElement.Escape(sourceCode), SecurityElement.Escape(referencedCode)); await VerifyItemWithReferenceWorkerAsync(xmlString, expectedOrderedItems, hideAdvancedMembers); } protected async Task VerifyItemWithReferenceWorkerAsync(string xmlString, IEnumerable<SignatureHelpTestItem> expectedOrderedItems, bool hideAdvancedMembers) { using var testWorkspace = EditorTestWorkspace.Create(xmlString); var cursorPosition = testWorkspace.Documents.First(d => d.Name == "SourceDocument").CursorPosition.Value; var documentId = testWorkspace.Documents.First(d => d.Name == "SourceDocument").Id; var document = testWorkspace.CurrentSolution.GetDocument(documentId); var options = new MemberDisplayOptions() with { HideAdvancedMembers = hideAdvancedMembers }; document = testWorkspace.CurrentSolution.GetDocument(documentId); var code = (await document.GetTextAsync()).ToString(); IList<TextSpan> textSpans = null; var selectedSpans = testWorkspace.Documents.First(d => d.Name == "SourceDocument").SelectedSpans; if (selectedSpans.Any()) { textSpans = selectedSpans; } TextSpan? textSpan = null; if (textSpans != null && textSpans.Any()) { textSpan = textSpans.First(); } await TestSignatureHelpWorkerSharedAsync(testWorkspace, code, cursorPosition, document, options, textSpan, expectedOrderedItems); } private async Task TestSignatureHelpWorkerSharedAsync( EditorTestWorkspace workspace, string code, int cursorPosition, Document document, MemberDisplayOptions options, TextSpan? textSpan, IEnumerable<SignatureHelpTestItem> expectedOrderedItemsOrNull = null, bool usePreviousCharAsTrigger = false) { var signatureHelpProviderType = GetSignatureHelpProviderType(); var signatureHelpProvider = workspace.ExportProvider.GetExportedValues<ISignatureHelpProvider>().Single(provider => provider.GetType() == signatureHelpProviderType); var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.InvokeSignatureHelpCommand); if (usePreviousCharAsTrigger) { triggerInfo = new SignatureHelpTriggerInfo( SignatureHelpTriggerReason.TypeCharCommand, code.ElementAt(cursorPosition - 1)); if (!signatureHelpProvider.TriggerCharacters.Contains(triggerInfo.TriggerCharacter.Value)) { return; } } var items = await signatureHelpProvider.GetItemsAsync(document, cursorPosition, triggerInfo, options, CancellationToken.None); // If we're expecting 0 items, then there's no need to compare them if ((expectedOrderedItemsOrNull == null || !expectedOrderedItemsOrNull.Any()) && items == null) { return; } AssertEx.NotNull(items, "Signature help provider returned null for items. Did you forget $$ in the test or is the test otherwise malformed, e.g. quotes not escaped?"); // Verify the span if (textSpan != null) { Assert.Equal(textSpan, items.ApplicableSpan); } if (expectedOrderedItemsOrNull != null) { CompareAndAssertCollectionsAndCurrentParameter(expectedOrderedItemsOrNull, items); CompareSelectedIndex(expectedOrderedItemsOrNull, items.SelectedItemIndex); } } private static void CompareSelectedIndex(IEnumerable<SignatureHelpTestItem> expectedOrderedItemsOrNull, int? selectedItemIndex) { if (expectedOrderedItemsOrNull == null || !expectedOrderedItemsOrNull.Any(i => i.IsSelected)) { return; } Assert.True(expectedOrderedItemsOrNull.Count(i => i.IsSelected) == 1, "Only one expected item can be marked with 'IsSelected'"); Assert.True(selectedItemIndex != null, "Expected an item to be selected, but no item was actually selected"); var counter = 0; foreach (var item in expectedOrderedItemsOrNull) { if (item.IsSelected) { Assert.True(selectedItemIndex == counter, $"Expected item with index {counter} to be selected, but the actual selected index is {selectedItemIndex}."); } else { Assert.True(selectedItemIndex != counter, $"Found unexpected selected item. Actual selected index is {selectedItemIndex}."); } counter++; } } protected async Task TestSignatureHelpWithMscorlib45Async( string markup, IEnumerable<SignatureHelpTestItem> expectedOrderedItems, string sourceLanguage) { var xmlString = string.Format(""" <Workspace> <Project Language="{0}" CommonReferencesNet45="true"> <Document FilePath="SourceDocument"> {1} </Document> </Project> </Workspace> """, sourceLanguage, SecurityElement.Escape(markup)); using var testWorkspace = EditorTestWorkspace.Create(xmlString); var cursorPosition = testWorkspace.Documents.Single(d => d.Name == "SourceDocument").CursorPosition.Value; var documentId = testWorkspace.Documents.Where(d => d.Name == "SourceDocument").Single().Id; var document = testWorkspace.CurrentSolution.GetDocument(documentId); var code = (await document.GetTextAsync()).ToString(); var options = new MemberDisplayOptions(); IList<TextSpan> textSpans = null; var selectedSpans = testWorkspace.Documents.Single(d => d.Name == "SourceDocument").SelectedSpans; if (selectedSpans.Any()) { textSpans = selectedSpans; } TextSpan? textSpan = null; if (textSpans != null && textSpans.Any()) { textSpan = textSpans.First(); } await TestSignatureHelpWorkerSharedAsync(testWorkspace, code, cursorPosition, document, options, textSpan, expectedOrderedItems); } }