Skip to content

Commit 77d5fe7

Browse files
Add embedded classification support for locals passed into an annotated api at a later point (#75875)
2 parents 24e78cf + f0933dc commit 77d5fe7

File tree

7 files changed

+290
-27
lines changed

7 files changed

+290
-27
lines changed

src/EditorFeatures/CSharpTest/Classification/SemanticClassifierTests_Json.cs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,35 @@ void Goo()
222222
Json.Array("]"));
223223
}
224224

225+
[Theory, CombinatorialData]
226+
public async Task TestJsonOnApiWithStringSyntaxAttribute_Field_FromLocal(TestHost testHost)
227+
{
228+
await TestAsync(
229+
"""
230+
using System.Diagnostics.CodeAnalysis;
231+
232+
class Program
233+
{
234+
[StringSyntax(StringSyntaxAttribute.Json)]
235+
private string field;
236+
void Goo()
237+
{
238+
[|var v = @"[{ 'goo': 0}]";|]
239+
this.field = v;
240+
}
241+
}
242+
""" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp,
243+
testHost,
244+
Keyword("var"),
245+
Json.Array("["),
246+
Json.Object("{"),
247+
Json.PropertyName("'goo'"),
248+
Json.Punctuation(":"),
249+
Json.Number("0"),
250+
Json.Object("}"),
251+
Json.Array("]"));
252+
}
253+
225254
[Theory, CombinatorialData]
226255
[WorkItem("https://github.com/dotnet/roslyn/issues/74020")]
227256
public async Task TestJsonOnApiWithStringSyntaxAttribute_OtherLanguage_Field(TestHost testHost)
@@ -302,6 +331,37 @@ void Goo()
302331
Json.Array("]"));
303332
}
304333

334+
[Theory, CombinatorialData]
335+
public async Task TestJsonOnApiWithStringSyntaxAttribute_Argument_FromLocal(TestHost testHost)
336+
{
337+
await TestAsync(
338+
"""
339+
using System.Diagnostics.CodeAnalysis;
340+
341+
class Program
342+
{
343+
private void M([StringSyntax(StringSyntaxAttribute.Json)] string p)
344+
{
345+
}
346+
347+
void Goo()
348+
{
349+
[|var v = @"[{ 'goo': 0}]";|]
350+
M(v);
351+
}
352+
}
353+
""" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp,
354+
testHost,
355+
Keyword("var"),
356+
Json.Array("["),
357+
Json.Object("{"),
358+
Json.PropertyName("'goo'"),
359+
Json.Punctuation(":"),
360+
Json.Number("0"),
361+
Json.Object("}"),
362+
Json.Array("]"));
363+
}
364+
305365
[Theory, CombinatorialData]
306366
[WorkItem("https://github.com/dotnet/roslyn/issues/69237")]
307367
public async Task TestJsonOnApiWithStringSyntaxAttribute_PropertyInitializer(TestHost testHost)
@@ -335,6 +395,40 @@ void Goo()
335395
Json.Array("]"));
336396
}
337397

398+
[Theory, CombinatorialData]
399+
[WorkItem("https://github.com/dotnet/roslyn/issues/69237")]
400+
public async Task TestJsonOnApiWithStringSyntaxAttribute_PropertyInitializer_FromLocal(TestHost testHost)
401+
{
402+
await TestAsync(
403+
""""
404+
using System.Diagnostics.CodeAnalysis;
405+
406+
public sealed record Foo
407+
{
408+
[StringSyntax(StringSyntaxAttribute.Json)]
409+
public required string Bar { get; set; }
410+
}
411+
412+
class Program
413+
{
414+
void Goo()
415+
{
416+
[|var v = """[1, 2, 3]""";|]
417+
var f = new Foo { Bar = v };
418+
}
419+
}
420+
"""" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp,
421+
testHost,
422+
Keyword("var"),
423+
Json.Array("["),
424+
Json.Number("1"),
425+
Json.Punctuation(","),
426+
Json.Number("2"),
427+
Json.Punctuation(","),
428+
Json.Number("3"),
429+
Json.Array("]"));
430+
}
431+
338432
[Theory, CombinatorialData]
339433
[WorkItem("https://github.com/dotnet/roslyn/issues/69237")]
340434
public async Task TestJsonOnApiWithStringSyntaxAttribute_WithExpression(TestHost testHost)
@@ -368,4 +462,75 @@ void Goo()
368462
Json.Number("3"),
369463
Json.Array("]"));
370464
}
465+
466+
[Theory, CombinatorialData]
467+
[WorkItem("https://github.com/dotnet/roslyn/issues/69237")]
468+
public async Task TestJsonOnApiWithStringSyntaxAttribute_WithExpression_FromLocal(TestHost testHost)
469+
{
470+
await TestAsync(
471+
""""
472+
using System.Diagnostics.CodeAnalysis;
473+
474+
public sealed record Foo
475+
{
476+
[StringSyntax(StringSyntaxAttribute.Json)]
477+
public required string Bar { get; set; }
478+
}
479+
480+
class Program
481+
{
482+
void Goo()
483+
{
484+
var f = new Foo { Bar = "" };
485+
[|var v = """[1, 2, 3]""";|]
486+
f = f with { Bar = v };
487+
}
488+
}
489+
"""" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp,
490+
testHost,
491+
Keyword("var"),
492+
Json.Array("["),
493+
Json.Number("1"),
494+
Json.Punctuation(","),
495+
Json.Number("2"),
496+
Json.Punctuation(","),
497+
Json.Number("3"),
498+
Json.Array("]"));
499+
}
500+
501+
[Theory, CombinatorialData]
502+
[WorkItem("https://github.com/dotnet/roslyn/issues/69237")]
503+
public async Task TestJsonOnApiWithStringSyntaxAttribute_WithExpression_FromLocal2(TestHost testHost)
504+
{
505+
await TestAsync(
506+
""""
507+
using System.Diagnostics.CodeAnalysis;
508+
509+
public sealed record Foo
510+
{
511+
[StringSyntax(StringSyntaxAttribute.Json)]
512+
public required string Bar { get; set; }
513+
}
514+
515+
class Program
516+
{
517+
void Goo()
518+
{
519+
var f = new Foo { Bar = "" };
520+
string v;
521+
[|v = """[1, 2, 3]""";|]
522+
f = f with { Bar = v };
523+
}
524+
}
525+
"""" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp,
526+
testHost,
527+
Local("v"),
528+
Json.Array("["),
529+
Json.Number("1"),
530+
Json.Punctuation(","),
531+
Json.Number("2"),
532+
Json.Punctuation(","),
533+
Json.Number("3"),
534+
Json.Array("]"));
535+
}
371536
}

src/Features/CSharp/Portable/EmbeddedLanguages/CSharpEmbeddedLanguagesProvider.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,16 @@
1313
namespace Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.LanguageServices;
1414

1515
[ExportLanguageService(typeof(IEmbeddedLanguagesProvider), LanguageNames.CSharp, ServiceLayer.Default), Shared]
16-
internal class CSharpEmbeddedLanguagesProvider : AbstractEmbeddedLanguagesProvider
16+
[method: ImportingConstructor]
17+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
18+
internal sealed class CSharpEmbeddedLanguagesProvider() : AbstractEmbeddedLanguagesProvider(Info)
1719
{
1820
public static readonly EmbeddedLanguageInfo Info = new(
21+
CSharpBlockFacts.Instance,
1922
CSharpSyntaxFacts.Instance,
2023
CSharpSemanticFactsService.Instance,
2124
CSharpVirtualCharService.Instance);
2225

23-
[ImportingConstructor]
24-
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
25-
public CSharpEmbeddedLanguagesProvider()
26-
: base(Info)
27-
{
28-
}
29-
3026
public override string EscapeText(string text, SyntaxToken token)
3127
=> EmbeddedLanguageUtilities.EscapeText(text, token);
3228
}

src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageDetector.cs

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
8+
using System.Diagnostics;
89
using System.Diagnostics.CodeAnalysis;
910
using System.Linq;
1011
using System.Threading;
@@ -198,21 +199,18 @@ private bool IsEmbeddedLanguageInterpolatedStringTextToken(
198199
return HasMatchingStringSyntaxAttribute(method.Parameters[0], out identifier);
199200
}
200201

201-
private bool IsEmbeddedLanguageStringLiteralToken(
202+
/// <summary>
203+
/// Checks for a string literal <em>directly</em> used in a location we can tell is controlled by a
204+
/// <c>[StringSyntax]</c> attribute.
205+
/// </summary>
206+
private bool IsEmbeddedLanguageStringLiteralToken_Direct(
202207
SyntaxToken token,
203208
SemanticModel semanticModel,
204209
CancellationToken cancellationToken,
205-
[NotNullWhen(true)] out string? identifier,
206-
out IEnumerable<string>? options)
210+
[NotNullWhen(true)] out string? identifier)
207211
{
208212
identifier = null;
209-
options = null;
210213
var syntaxFacts = Info.SyntaxFacts;
211-
if (!syntaxFacts.IsLiteralExpression(token.Parent))
212-
return false;
213-
214-
if (HasLanguageComment(token, syntaxFacts, out identifier, out options))
215-
return true;
216214

217215
// If we're a string used in a collection initializer, treat this as a lang string if the collection itself
218216
// is properly annotated. This is for APIs that do things like DateTime.ParseExact(..., string[] formats, ...);
@@ -276,6 +274,111 @@ private bool IsEmbeddedLanguageStringLiteralToken(
276274
return false;
277275
}
278276

277+
private bool IsEmbeddedLanguageStringLiteralToken(
278+
SyntaxToken token,
279+
SemanticModel semanticModel,
280+
CancellationToken cancellationToken,
281+
[NotNullWhen(true)] out string? identifier,
282+
out IEnumerable<string>? options)
283+
{
284+
identifier = null;
285+
options = null;
286+
var syntaxFacts = Info.SyntaxFacts;
287+
if (!syntaxFacts.IsLiteralExpression(token.Parent))
288+
return false;
289+
290+
if (HasLanguageComment(token, syntaxFacts, out identifier, out options))
291+
return true;
292+
293+
// Check for *direct* usage of this token that indicates it's an embedded language string. Like passing it to
294+
// an argument which has the StringSyntax attribute on it.
295+
if (IsEmbeddedLanguageStringLiteralToken_Direct(
296+
token, semanticModel, cancellationToken, out identifier))
297+
{
298+
return true;
299+
}
300+
301+
// Now check for if the literal was assigned to a local that we then see is passed along to something that
302+
// indicates an embedded language string at some later point.
303+
304+
var container = TryFindContainer(token);
305+
if (container is null)
306+
return false;
307+
308+
var statement = container.FirstAncestorOrSelf<SyntaxNode>(syntaxFacts.IsStatement);
309+
if (syntaxFacts.IsSimpleAssignmentStatement(statement))
310+
{
311+
syntaxFacts.GetPartsOfAssignmentStatement(statement, out var left, out var right);
312+
return container == right &&
313+
IsLocalConsumedByApiWithStringSyntaxAttribute(
314+
semanticModel.GetSymbolInfo(left, cancellationToken).GetAnySymbol(), container, semanticModel, cancellationToken, out identifier);
315+
}
316+
317+
if (syntaxFacts.IsEqualsValueClause(container.Parent) &&
318+
syntaxFacts.IsVariableDeclarator(container.Parent.Parent))
319+
{
320+
var variableDeclarator = container.Parent.Parent;
321+
var symbol =
322+
semanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken) ??
323+
semanticModel.GetDeclaredSymbol(syntaxFacts.GetIdentifierOfVariableDeclarator(variableDeclarator).GetRequiredParent(), cancellationToken);
324+
325+
return IsLocalConsumedByApiWithStringSyntaxAttribute(symbol, container, semanticModel, cancellationToken, out identifier);
326+
}
327+
328+
return false;
329+
}
330+
331+
private bool IsLocalConsumedByApiWithStringSyntaxAttribute(
332+
ISymbol? symbol,
333+
SyntaxNode tokenParent,
334+
SemanticModel semanticModel,
335+
CancellationToken cancellationToken,
336+
[NotNullWhen(true)] out string? identifier)
337+
{
338+
identifier = null;
339+
if (symbol is not ILocalSymbol localSymbol)
340+
return false;
341+
342+
var blockFacts = this.Info.BlockFacts;
343+
var syntaxFacts = this.Info.SyntaxFacts;
344+
345+
var block = tokenParent.AncestorsAndSelf().FirstOrDefault(blockFacts.IsExecutableBlock);
346+
if (block is null)
347+
return false;
348+
349+
var localName = localSymbol.Name;
350+
if (localName == "")
351+
return false;
352+
353+
// Now look at the next statements that follow for usages of this local variable.
354+
foreach (var statement in blockFacts.GetExecutableBlockStatements(block))
355+
{
356+
foreach (var descendent in statement.DescendantNodesAndSelf())
357+
{
358+
cancellationToken.ThrowIfCancellationRequested();
359+
360+
if (!syntaxFacts.IsIdentifierName(descendent))
361+
continue;
362+
363+
var identifierToken = syntaxFacts.GetIdentifierOfIdentifierName(descendent);
364+
if (identifierToken.ValueText != localName)
365+
continue;
366+
367+
var otherSymbol = semanticModel.GetSymbolInfo(descendent, cancellationToken).GetAnySymbol();
368+
369+
// Only do a direct check here. We don't want to continually do indirect checks where a string literal
370+
// is assigned to one local, assigned to another local, assigned to another local, and so on.
371+
if (localSymbol.Equals(otherSymbol) &&
372+
IsEmbeddedLanguageStringLiteralToken_Direct(identifierToken, semanticModel, cancellationToken, out identifier))
373+
{
374+
return true;
375+
}
376+
}
377+
}
378+
379+
return false;
380+
}
381+
279382
private SyntaxNode? TryFindContainer(SyntaxToken token)
280383
{
281384
var syntaxFacts = Info.SyntaxFacts;

src/Features/Core/Portable/EmbeddedLanguages/EmbeddedLanguageInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ namespace Microsoft.CodeAnalysis.EmbeddedLanguages;
1111

1212
internal readonly struct EmbeddedLanguageInfo
1313
{
14+
public readonly IBlockFacts BlockFacts;
1415
public readonly ISyntaxFacts SyntaxFacts;
1516
public readonly ISemanticFactsService SemanticFacts;
1617
public readonly IVirtualCharService VirtualCharService;
1718

1819
public readonly ISyntaxKinds SyntaxKinds => SyntaxFacts.SyntaxKinds;
1920

2021
public EmbeddedLanguageInfo(
22+
IBlockFacts blockFacts,
2123
ISyntaxFacts syntaxFacts,
2224
ISemanticFactsService semanticFacts,
2325
IVirtualCharService virtualCharService)
2426
{
27+
BlockFacts = blockFacts;
2528
SyntaxFacts = syntaxFacts;
2629
SemanticFacts = semanticFacts;
2730
VirtualCharService = virtualCharService;

src/Features/VisualBasic/Portable/EmbeddedLanguages/VisualBasicEmbeddedLanguagesProvider.vb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService
1111

1212
Namespace Microsoft.CodeAnalysis.VisualBasic.EmbeddedLanguages.LanguageServices
1313
<ExportLanguageService(GetType(IEmbeddedLanguagesProvider), LanguageNames.VisualBasic, ServiceLayer.Default), [Shared]>
14-
Friend Class VisualBasicEmbeddedLanguagesProvider
14+
Friend NotInheritable Class VisualBasicEmbeddedLanguagesProvider
1515
Inherits AbstractEmbeddedLanguagesProvider
1616

1717
Public Shared ReadOnly Info As New EmbeddedLanguageInfo(
18+
VisualBasicBlockFacts.Instance,
1819
VisualBasicSyntaxFacts.Instance,
1920
VisualBasicSemanticFactsService.Instance,
2021
VisualBasicVirtualCharService.Instance)

0 commit comments

Comments
 (0)