|
// 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.
using System;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Completion.Providers;
internal abstract class AbstractInternalsVisibleToCompletionProvider : LSPCompletionProvider
{
private const string ProjectGuidKey = nameof(ProjectGuidKey);
protected abstract IImmutableList<SyntaxNode> GetAssemblyScopedAttributeSyntaxNodesOfDocument(SyntaxNode documentRoot);
protected abstract SyntaxNode? GetConstructorArgumentOfInternalsVisibleToAttribute(SyntaxNode internalsVisibleToAttribute);
public sealed override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options)
{
// Should trigger in these cases ($$ is the cursor position)
// [InternalsVisibleTo($$ -> user enters "
// [InternalsVisibleTo("$$")] -> user enters any character
var ch = text[insertedCharacterPosition];
if (ch == '\"')
{
return true;
}
else
{
if (insertedCharacterPosition > 0)
{
ch = text[insertedCharacterPosition - 1];
if (ch == '\"')
{
return ShouldTriggerAfterQuotes(text, insertedCharacterPosition);
}
}
}
return false;
}
protected abstract bool ShouldTriggerAfterQuotes(SourceText text, int insertedCharacterPosition);
public override ImmutableHashSet<char> TriggerCharacters { get; } = ['\"'];
public override async Task ProvideCompletionsAsync(CompletionContext context)
{
try
{
var cancellationToken = context.CancellationToken;
var syntaxTree = await context.Document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var syntaxFactsService = context.Document.GetRequiredLanguageService<ISyntaxFactsService>();
if (syntaxFactsService.IsEntirelyWithinStringOrCharOrNumericLiteral(syntaxTree, context.Position, cancellationToken))
{
var token = syntaxTree.FindTokenOnLeftOfPosition(context.Position, cancellationToken);
var attributeSyntaxNode = GetAttributeSyntaxNodeOfToken(syntaxFactsService, token);
if (attributeSyntaxNode == null)
{
return;
}
if (await CheckTypeInfoOfAttributeAsync(context.Document, attributeSyntaxNode, context.CancellationToken).ConfigureAwait(false))
{
await AddAssemblyCompletionItemsAsync(context, cancellationToken).ConfigureAwait(false);
}
}
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
{
// nop
}
}
private static SyntaxNode? GetAttributeSyntaxNodeOfToken(ISyntaxFactsService syntaxFactsService, SyntaxToken token)
{
//Supported cases:
//[Attribute("|
//[Attribute(parameterName:"Text|")
//Also supported but excluded by IsPositionEntirelyWithinStringLiteral in ProvideCompletionsAsync
//[Attribute(""|
//[Attribute("Text"|)
var node = token.Parent;
if (node != null && syntaxFactsService.IsStringLiteralExpression(node))
{
// Edge cases:
// ElementAccessExpressionSyntax is present if the following statement is another attribute:
// [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("|
// [assembly: System.Reflection.AssemblyVersion("1.0.0.0")]
// [assembly: System.Reflection.AssemblyCompany("Test")]
// BinaryExpression is present if the string literal is concatenated:
// From: https://msdn.microsoft.com/de-de/library/system.runtime.compilerservices.internalsvisibletoattribute(v=vs.110).aspx
// [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Friend1, PublicKey=002400000480000094" +
// "0000000602000000240000525341310004000" + ..
while (syntaxFactsService.IsElementAccessExpression(node.Parent) || syntaxFactsService.IsBinaryExpression(node.Parent))
{
node = node.Parent;
}
// node -> AttributeArgumentSyntax -> AttributeArgumentListSyntax -> AttributeSyntax
var attributeSyntaxNodeCandidate = node.Parent?.Parent?.Parent;
if (syntaxFactsService.IsAttribute(attributeSyntaxNodeCandidate))
{
return attributeSyntaxNodeCandidate;
}
}
return null;
}
private static async Task<bool> CheckTypeInfoOfAttributeAsync(Document document, SyntaxNode attributeNode, CancellationToken cancellationToken)
{
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(attributeNode, cancellationToken).ConfigureAwait(false);
var typeInfo = semanticModel.GetTypeInfo(attributeNode, cancellationToken);
var type = typeInfo.Type;
if (type == null)
{
return false;
}
var internalsVisibleToAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(typeof(InternalsVisibleToAttribute).FullName!);
return type.Equals(internalsVisibleToAttributeSymbol);
}
private async Task AddAssemblyCompletionItemsAsync(CompletionContext context, CancellationToken cancellationToken)
{
var currentProject = context.Document.Project;
var allInternalsVisibleToAttributesOfProject = await GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(context, cancellationToken).ConfigureAwait(false);
foreach (var project in context.Document.Project.Solution.Projects)
{
if (project == currentProject)
{
continue;
}
if (IsProjectTypeUnsupported(project))
{
continue;
}
if (allInternalsVisibleToAttributesOfProject.Contains(project.AssemblyName))
{
continue;
}
var projectGuid = project.Id.Id.ToString();
var completionItem = CommonCompletionItem.Create(
displayText: project.AssemblyName,
displayTextSuffix: "",
rules: CompletionItemRules.Default,
glyph: project.GetGlyph(),
properties: [KeyValuePairUtil.Create(ProjectGuidKey, projectGuid)]);
context.AddItem(completionItem);
}
if (context.Items.Count > 0)
{
context.CompletionListSpan = await GetTextChangeSpanAsync(
context.Document, context.CompletionListSpan, cancellationToken).ConfigureAwait(false);
}
}
private static bool IsProjectTypeUnsupported(Project project)
=> !project.SupportsCompilation;
private async Task<IImmutableSet<string>> GetAllInternalsVisibleToAssemblyNamesOfProjectAsync(CompletionContext completionContext, CancellationToken cancellationToken)
{
// Looking up other InternalsVisibleTo attributes of this project. This is faster than compiling all projects of the solution and checking access via
// sourceAssembly.GivesAccessTo(compilation.Assembly)
// at the cost of being not so precise (can't check the validity of the PublicKey).
var project = completionContext.Document.Project;
var resultBuilder = (ImmutableHashSet<string>.Builder?)null;
foreach (var document in project.Documents)
{
var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var assemblyScopedAttributes = GetAssemblyScopedAttributeSyntaxNodesOfDocument(syntaxRoot);
foreach (var attribute in assemblyScopedAttributes)
{
// Skip attributes with errors. This skips the attribute that is currently edited, until it is complete:
// [assembly: InternalsVisibleTo("$$
// CS1003: Syntax error, ']' expected; CS1010: A string was not properly delimited; CS1026: An incomplete statement was found
// see also SyntaxNode.HasErrors
if (attribute.ContainsDiagnostics)
{
foreach (var diagnostic in attribute.GetDiagnostics())
{
if (diagnostic.Severity == DiagnosticSeverity.Error)
{
continue;
}
}
}
if (await CheckTypeInfoOfAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false))
{
// See Microsoft.CodeAnalysis.PEAssembly.BuildInternalsVisibleToMap for reference on how
// the 'real' InternalsVisibleTo logic extracts and compares the assemblyName:
// * Extract the assemblyName by AssemblyIdentity.TryParseDisplayName
// * Compare with StringComparer.OrdinalIgnoreCase
// We take the same approach, but we do only a limited check of the PublicKey.
// The PublicKey is checked by AssemblyIdentity.TryParseDisplayName to be
// parseable (length, can be converted to bytes, etc.), but it is not tested whether
// the public key actually fits to the assembly.
var assemblyName = await GetAssemblyNameFromInternalsVisibleToAttributeAsync(document, attribute, completionContext.CancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(assemblyName))
{
resultBuilder ??= ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
resultBuilder.Add(assemblyName);
}
}
}
}
return resultBuilder == null
? []
: resultBuilder.ToImmutable();
}
private async Task<string> GetAssemblyNameFromInternalsVisibleToAttributeAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
{
var constructorArgument = GetConstructorArgumentOfInternalsVisibleToAttribute(node);
if (constructorArgument == null)
{
return string.Empty;
}
var semanticModel = await document.ReuseExistingSpeculativeModelAsync(constructorArgument, cancellationToken).ConfigureAwait(false);
var constantCandidate = semanticModel.GetConstantValue(constructorArgument, cancellationToken);
if (constantCandidate.HasValue && constantCandidate.Value is string argument)
{
if (AssemblyIdentity.TryParseDisplayName(argument, out var assemblyIdentity))
{
return assemblyIdentity.Name;
}
}
return string.Empty;
}
private static async Task<TextSpan> GetTextChangeSpanAsync(Document document, TextSpan startSpan, CancellationToken cancellationToken)
{
var result = startSpan;
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(result.Start);
if (syntaxFacts.IsStringLiteral(token) || syntaxFacts.IsVerbatimStringLiteral(token))
{
var text = root.GetText();
// Expand selection in both directions until a double quote or any line break character is reached
static bool IsWordCharacter(char ch) => !(ch == '"' || TextUtilities.IsAnyLineBreakCharacter(ch));
result = CommonCompletionUtilities.GetWordSpan(
text, startSpan.Start, IsWordCharacter, IsWordCharacter, alwaysExtendEndSpan: true);
}
return result;
}
public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default)
{
var projectIdGuid = item.GetProperty(ProjectGuidKey);
var projectId = ProjectId.CreateFromSerialized(new Guid(projectIdGuid));
var project = document.Project.Solution.GetRequiredProject(projectId);
var assemblyName = item.DisplayText;
var publicKey = await GetPublicKeyOfProjectAsync(project, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(publicKey))
{
assemblyName += $", PublicKey={publicKey}";
}
var textChange = new TextChange(item.Span, assemblyName);
return CompletionChange.Create(textChange);
}
private static async Task<string> GetPublicKeyOfProjectAsync(Project project, CancellationToken cancellationToken)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
if (compilation?.Assembly?.Identity?.IsStrongName == true)
{
return GetPublicKeyAsHexString(compilation.Assembly.Identity.PublicKey);
}
return string.Empty;
}
private static string GetPublicKeyAsHexString(ImmutableArray<byte> publicKey)
{
var pooledStrBuilder = PooledStringBuilder.GetInstance();
var builder = pooledStrBuilder.Builder;
foreach (var b in publicKey)
{
builder.Append(b.ToString("x2"));
}
return pooledStrBuilder.ToStringAndFree();
}
}
|