Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix exception for non-cs/vb document #72677

Merged
merged 5 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private async Task ProduceTagsAsync(
// and hence only report them for 'DiagnosticKind.AnalyzerSemantic'.
if (_diagnosticKind == DiagnosticKind.AnalyzerSemantic)
{
var copilotDiagnostics = await document.GetCachedCopilotDiagnosticsAsync(cancellationToken).ConfigureAwait(false);
var copilotDiagnostics = await document.GetCachedCopilotDiagnosticsAsync(requestedSpan.Span.ToTextSpan(), cancellationToken).ConfigureAwait(false);
diagnostics = diagnostics.AddRange(copilotDiagnostics);
}

Expand Down
3 changes: 1 addition & 2 deletions src/EditorFeatures/Core/Copilot/CopilotTaggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ protected override async Task ProduceTagsAsync(TaggerContext<ITextMarkerTag> con
var prompts = await service.GetAvailablePromptTitlesAsync(document, cancellationToken).ConfigureAwait(false);
if (prompts.Length > 0)
{
// Invoke analysis call into the Copilot service for the containing method's span.
await service.AnalyzeDocumentAsync(document, new(spanToTag.SnapshotSpan.Start, 0), prompts[0], cancellationToken).ConfigureAwait(false);
await service.AnalyzeDocumentAsync(document, spanToTag.SnapshotSpan.Span.ToTextSpan(), prompts[0], cancellationToken).ConfigureAwait(false);
}
}
}
84 changes: 84 additions & 0 deletions src/EditorFeatures/Test2/CodeFixes/CodeFixServiceTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Imports System.Reflection
Imports System.Threading
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.CodeFixes
Imports Microsoft.CodeAnalysis.Copilot
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests
Imports Microsoft.CodeAnalysis.Editor.UnitTests
Expand All @@ -17,6 +18,8 @@ Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.CodeAnalysis.ErrorLogger
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.UnitTests
Imports Roslyn.Utilities

Namespace Microsoft.CodeAnalysis.Editor.Implementation.CodeFixes.UnitTests
Expand Down Expand Up @@ -263,5 +266,86 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.CodeFixes.UnitTests
#Enable Warning RS0005
End Function
End Class

<Fact>
Public Async Function TestCopilotCodeAnalysisServiceWithoutSyntaxTree() As Task
Dim workspaceDefinition =
<Workspace>
<Project Language="NoCompilation" AssemblyName="TestAssembly" CommonReferencesPortable="true">
<Document>
var x = {}; // e.g., TypeScript code or anything else that doesn't support compilations
</Document>
</Project>
</Workspace>

Dim composition = EditorTestCompositions.EditorFeatures.AddParts(
GetType(NoCompilationContentTypeDefinitions),
GetType(NoCompilationContentTypeLanguageService),
GetType(NoCompilationCopilotCodeAnalysisService))

Using workspace = EditorTestWorkspace.Create(workspaceDefinition, composition:=composition)

Dim document = workspace.CurrentSolution.Projects.Single().Documents.Single()
Dim diagnosticsXml =
<Diagnostics>
<Error Id=<%= "TestId" %>
MappedFile=<%= document.Name %> MappedLine="0" MappedColumn="0"
OriginalFile=<%= document.Name %> OriginalLine="0" OriginalColumn="0"
Message=<%= "Test Message" %>/>
</Diagnostics>
Dim diagnostics = DiagnosticProviderTests.GetExpectedDiagnostics(workspace, diagnosticsXml)

Dim copilotCodeAnalysisService = document.Project.Services.GetService(Of ICopilotCodeAnalysisService)()
Dim noCompilationCopilotCodeAnalysisService = DirectCast(copilotCodeAnalysisService, NoCompilationCopilotCodeAnalysisService)

NoCompilationCopilotCodeAnalysisService.Diagnostics = diagnostics.SelectAsArray(Of Diagnostic)(
Function(d) d.ToDiagnosticAsync(document.Project, CancellationToken.None).Result)
Dim codefixService = workspace.ExportProvider.GetExportedValue(Of ICodeFixService)

' Make sure we don't crash
Dim unused = Await codefixService.GetMostSevereFixAsync(
document, Text.TextSpan.FromBounds(0, 0), New DefaultCodeActionRequestPriorityProvider(), CodeActionOptions.DefaultProvider, CancellationToken.None)
End Using
End Function

<ExportLanguageService(GetType(ICopilotCodeAnalysisService), NoCompilationConstants.LanguageName, ServiceLayer.Test), [Shared], PartNotDiscoverable>
Private Class NoCompilationCopilotCodeAnalysisService
Implements ICopilotCodeAnalysisService

<ImportingConstructor>
<Obsolete(MefConstruction.ImportingConstructorMessage, True)>
Public Sub New()
End Sub

Public Shared Property Diagnostics As ImmutableArray(Of Diagnostic) = ImmutableArray(Of Diagnostic).Empty

Public Function IsRefineOptionEnabledAsync() As Task(Of Boolean) Implements ICopilotCodeAnalysisService.IsRefineOptionEnabledAsync
Return Task.FromResult(True)
End Function

Public Function IsCodeAnalysisOptionEnabledAsync() As Task(Of Boolean) Implements ICopilotCodeAnalysisService.IsCodeAnalysisOptionEnabledAsync
Return Task.FromResult(True)
End Function

Public Function IsAvailableAsync(cancellationToken As CancellationToken) As Task(Of Boolean) Implements ICopilotCodeAnalysisService.IsAvailableAsync
Return Task.FromResult(True)
End Function

Public Function GetAvailablePromptTitlesAsync(document As Document, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of String)) Implements ICopilotCodeAnalysisService.GetAvailablePromptTitlesAsync
Return Task.FromResult(ImmutableArray.Create("Title"))
End Function

Public Function AnalyzeDocumentAsync(document As Document, span As TextSpan?, promptTitle As String, cancellationToken As CancellationToken) As Task Implements ICopilotCodeAnalysisService.AnalyzeDocumentAsync
Return Task.CompletedTask
End Function

Public Function GetCachedDocumentDiagnosticsAsync(document As Document, span As TextSpan?, promptTitles As ImmutableArray(Of String), cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of Diagnostic)) Implements ICopilotCodeAnalysisService.GetCachedDocumentDiagnosticsAsync
Return Task.FromResult(Diagnostics)
End Function

Public Function StartRefinementSessionAsync(oldDocument As Document, newDocument As Document, primaryDiagnostic As Diagnostic, cancellationToken As CancellationToken) As Task Implements ICopilotCodeAnalysisService.StartRefinementSessionAsync
Return Task.CompletedTask
End Function
End Class
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests
Return analyzerService
End Function

Private Shared Function GetExpectedDiagnostics(workspace As EditorTestWorkspace, diagnostics As XElement) As List(Of DiagnosticData)
Friend Shared Function GetExpectedDiagnostics(workspace As EditorTestWorkspace, diagnostics As XElement) As List(Of DiagnosticData)
Dim result As New List(Of DiagnosticData)
Dim mappedLine As Integer, mappedColumn As Integer, originalLine As Integer, originalColumn As Integer
Dim Id As String, message As String, originalFile As String, mappedFile As String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -71,20 +70,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

var hasMultiplePrompts = promptTitles.Length > 1;

// Find the containing method, if any, and also update the fix span to the entire method.
// TODO: count location in doc-comment as part of the method.
// Find the containing method for each diagnostic, and register a fix if any part of the method interect with context span.
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var containingMethod = CSharpSyntaxFacts.Instance.GetContainingMethodDeclaration(root, context.Span.Start, useFullSpan: false);
if (containingMethod is not BaseMethodDeclarationSyntax)
return;

foreach (var diagnostic in context.Diagnostics)
{
Debug.Assert(containingMethod.FullSpan.IntersectsWith(diagnostic.Location.SourceSpan));

var fix = TryGetFix(document, containingMethod, diagnostic, hasMultiplePrompts);
if (fix != null)
context.RegisterCodeFix(fix, diagnostic);
var containingMethod = CSharpSyntaxFacts.Instance.GetContainingMethodDeclaration(root, diagnostic.Location.SourceSpan.Start, useFullSpan: false);
if (containingMethod?.Span.IntersectsWith(context.Span) is true)
{
var fix = TryGetFix(document, containingMethod, diagnostic, hasMultiplePrompts);
if (fix != null)
context.RegisterCodeFix(fix, diagnostic);
}
}
}

Expand All @@ -105,8 +101,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
// Parse the proposed Copilot fix into a method declaration.
// Guard against failure cases where the proposed fixed code does not parse into a method declaration.
// TODO: consider do this early when we create the diagnostic and add a flag in the property bag to speedup lightbulb computation
var fixMethodDeclaration = SyntaxFactory.ParseMemberDeclaration(fix, options: method.SyntaxTree.Options);
if (fixMethodDeclaration is null || !fixMethodDeclaration.IsKind(SyntaxKind.MethodDeclaration) || fixMethodDeclaration.GetDiagnostics().Count() > 3)
var memberDeclaration = SyntaxFactory.ParseMemberDeclaration(fix, options: method.SyntaxTree.Options);
if (memberDeclaration is null || memberDeclaration is not BaseMethodDeclarationSyntax baseMethodDeclaration || baseMethodDeclaration.GetDiagnostics().Count() > 3)
return null;

var title = hasMultiplePrompts
Expand All @@ -125,9 +121,9 @@ async Task<Document> GetFixedDocumentAsync(SyntaxNode method, string fix, Cancel
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// TODO: Replace all the whitespace trivia with elastic trivia, and any other trivia related improvements
var newMethod = fixMethodDeclaration
.WithLeadingTrivia(fixMethodDeclaration.HasLeadingTrivia ? fixMethodDeclaration.GetLeadingTrivia() : method.GetLeadingTrivia())
.WithTrailingTrivia(fixMethodDeclaration.HasTrailingTrivia ? fixMethodDeclaration.GetTrailingTrivia() : method.GetTrailingTrivia())
var newMethod = memberDeclaration
.WithLeadingTrivia(memberDeclaration.HasLeadingTrivia ? memberDeclaration.GetLeadingTrivia() : method.GetLeadingTrivia())
.WithTrailingTrivia(memberDeclaration.HasTrailingTrivia ? memberDeclaration.GetTrailingTrivia() : method.GetTrailingTrivia())
.WithAdditionalAnnotations(Formatter.Annotation, WarningAnnotation);

editor.ReplaceNode(method, newMethod);
Expand Down
18 changes: 4 additions & 14 deletions src/Features/Core/Portable/Copilot/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,17 @@ namespace Microsoft.CodeAnalysis.Copilot;

internal static class Extensions
{
public static async Task<ImmutableArray<DiagnosticData>> GetCachedCopilotDiagnosticsAsync(this TextDocument document, TextSpan span, CancellationToken cancellationToken)
{
var diagnostics = await document.GetCachedCopilotDiagnosticsAsync(cancellationToken).ConfigureAwait(false);
if (diagnostics.IsEmpty)
return [];

var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
return diagnostics.WhereAsArray(diagnostic => span.IntersectsWith(diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text)));
}

public static async Task<ImmutableArray<DiagnosticData>> GetCachedCopilotDiagnosticsAsync(this TextDocument document, CancellationToken cancellationToken)
public static async Task<ImmutableArray<DiagnosticData>> GetCachedCopilotDiagnosticsAsync(this TextDocument document, TextSpan? span, CancellationToken cancellationToken)
{
if (document is not Document sourceDocument)
return ImmutableArray<DiagnosticData>.Empty;
return [];

var copilotCodeAnalysisService = sourceDocument.GetLanguageService<ICopilotCodeAnalysisService>();
if (copilotCodeAnalysisService is null)
return ImmutableArray<DiagnosticData>.Empty;
return [];

var promptTitles = await copilotCodeAnalysisService.GetAvailablePromptTitlesAsync(sourceDocument, cancellationToken).ConfigureAwait(false);
var copilotDiagnostics = await copilotCodeAnalysisService.GetCachedDocumentDiagnosticsAsync(sourceDocument, promptTitles, cancellationToken).ConfigureAwait(false);
var copilotDiagnostics = await copilotCodeAnalysisService.GetCachedDocumentDiagnosticsAsync(sourceDocument, span, promptTitles, cancellationToken).ConfigureAwait(false);
return copilotDiagnostics.SelectAsArray(d => DiagnosticData.Create(d, sourceDocument));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal interface ICopilotCodeAnalysisService : ILanguageService
/// <remarks>
/// A prompt's title serves as the ID of the prompt, which can be used to selectively trigger analysis and retrive cached results.
/// </remarks>
Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(Document document, ImmutableArray<string> promptTitles, CancellationToken cancellationToken);
Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(Document document, TextSpan? span, ImmutableArray<string> promptTitles, CancellationToken cancellationToken);

/// <summary>
/// Method to start a Copilot refinement session on top of the changes between the given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
Expand Down Expand Up @@ -255,20 +254,10 @@ private static async Task<ImmutableArray<DiagnosticData>> GetCopilotDiagnosticsA
CodeActionRequestPriority? priority,
CancellationToken cancellationToken)
{
if (!(priority is null or CodeActionRequestPriority.Low)
|| document is not Document sourceDocument)
{
return [];
}

// Expand the fixable range for Copilot diagnostics to containing method.
// TODO: Share the below code with other places we compute containing method for Copilot analysis.
var root = await sourceDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var syntaxFacts = sourceDocument.GetRequiredLanguageService<ISyntaxFactsService>();
var containingMethod = syntaxFacts.GetContainingMethodDeclaration(root, range.Start, useFullSpan: false);
range = containingMethod?.Span ?? range;
if (priority is null or CodeActionRequestPriority.Low)
return await document.GetCachedCopilotDiagnosticsAsync(range, cancellationToken).ConfigureAwait(false);

return await document.GetCachedCopilotDiagnosticsAsync(range, cancellationToken).ConfigureAwait(false);
return [];
}

private static SortedDictionary<TextSpan, List<DiagnosticData>> ConvertToMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public override async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(
// Add cached Copilot diagnostics when computing analyzer semantic diagnostics.
if (DiagnosticKind == DiagnosticKind.AnalyzerSemantic)
{
var copilotDiagnostics = await Document.GetCachedCopilotDiagnosticsAsync(cancellationToken).ConfigureAwait(false);
var copilotDiagnostics = await Document.GetCachedCopilotDiagnosticsAsync(span: null, cancellationToken).ConfigureAwait(false);
allSpanDiagnostics = allSpanDiagnostics.AddRange(copilotDiagnostics);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Threading;
Expand Down Expand Up @@ -116,7 +117,7 @@ private void CacheAndRefreshDiagnosticsIfNeeded(Document document, string prompt
diagnosticsRefresher.RequestWorkspaceRefresh();
}

public async Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(Document document, ImmutableArray<string> promptTitles, CancellationToken cancellationToken)
public async Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(Document document, TextSpan? span, ImmutableArray<string> promptTitles, CancellationToken cancellationToken)
{
if (await ShouldSkipAnalysisAsync(document, cancellationToken).ConfigureAwait(false))
return [];
Expand Down Expand Up @@ -144,9 +145,17 @@ public async Task<ImmutableArray<Diagnostic>> GetCachedDocumentDiagnosticsAsync(
}
}

if (span.HasValue)
return await GetDiagnosticsIntersectWithSpanAsync(document, diagnostics, span.Value, cancellationToken).ConfigureAwait(false);

return diagnostics.ToImmutable();
}

protected virtual Task<ImmutableArray<Diagnostic>> GetDiagnosticsIntersectWithSpanAsync(Document document, IReadOnlyList<Diagnostic> diagnostics, TextSpan span, CancellationToken cancellationToken)
{
return Task.FromResult(diagnostics.WhereAsArray((diagnostic, _) => diagnostic.Location.SourceSpan.IntersectsWith(span), state: (object)null));
}

public async Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken)
{
if (await IsRefineOptionEnabledAsync().ConfigureAwait(false))
Expand Down
Loading
Loading