Skip to content
Open
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
12 changes: 12 additions & 0 deletions docfx/analyzers/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ all use `vs-threading.TopicA.txt` as the filename for their `AdditionalFiles` it

These files may contain blank lines or comments that start with the `#` character.

Files that contain a backtick use metadata type names. In these files, generic types include
a backtick followed by the number of type parameters. For example, the metadata name for
`DbSet<TEntity>` is ``DbSet`1``. Including the arity distinguishes a generic type from a
non-generic type with the same name.

For backward compatibility, files without any backticks match type names without regard
to generic arity. For example, `DbSet` in such a file matches both a non-generic `DbSet`
and every generic `DbSet<T...>` type. Add a backtick anywhere in the file to opt into
exact metadata-name matching for every entry in that file.

## Methods that assert the main thread

Code may assert it is running on the main thread by calling a method that is designed
Expand Down Expand Up @@ -92,3 +102,5 @@ excluded from VSTHRD103 analysis by specifying them in a configuration file.
**Line format:** `[Namespace.TypeName]::MethodName`

**Sample:** `[System.Data.SqlClient.SqlDataReader]::Read`

**Generic sample:** ``[Microsoft.EntityFrameworkCore.DbSet`1]::Add``
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ internal static class CommonFixes
internal static async Task<ImmutableArray<QualifiedMember>> ReadMethodsAsync(CodeFixContext codeFixContext, Regex fileNamePattern, CancellationToken cancellationToken)
{
ImmutableArray<QualifiedMember>.Builder? result = ImmutableArray.CreateBuilder<QualifiedMember>();
foreach (string line in await ReadAdditionalFilesAsync(codeFixContext.Document.Project.AdditionalDocuments, fileNamePattern, cancellationToken))
foreach (SourceText text in await ReadAdditionalFileTextsAsync(codeFixContext.Document.Project.AdditionalDocuments, fileNamePattern, cancellationToken))
{
result.Add(ParseAdditionalFileMethodLine(line));
bool matchAnyArity = !Contains(text, '`');
foreach (string line in ReadLinesFromAdditionalFile(text))
{
result.Add(ParseAdditionalFileMethodLine(line, matchAnyArity));
}
}

return result.ToImmutable();
}

internal static async Task<ImmutableArray<string>> ReadAdditionalFilesAsync(IEnumerable<TextDocument> additionalFiles, Regex fileNamePattern, CancellationToken cancellationToken)
internal static async Task<ImmutableArray<SourceText>> ReadAdditionalFileTextsAsync(IEnumerable<TextDocument> additionalFiles, Regex fileNamePattern, CancellationToken cancellationToken)
{
if (additionalFiles is null)
{
Expand All @@ -50,11 +54,11 @@ internal static async Task<ImmutableArray<string>> ReadAdditionalFilesAsync(IEnu
let fileName = Path.GetFileName(doc.Name)
where fileNamePattern.IsMatch(fileName)
select doc;
ImmutableArray<string>.Builder? result = ImmutableArray.CreateBuilder<string>();
ImmutableArray<SourceText>.Builder? result = ImmutableArray.CreateBuilder<SourceText>();
foreach (TextDocument? doc in docs)
{
SourceText? text = await doc.GetTextAsync(cancellationToken);
result.AddRange(ReadLinesFromAdditionalFile(text));
SourceText text = await doc.GetTextAsync(cancellationToken);
result.Add(text);
}

return result.ToImmutable();
Expand Down
111 changes: 89 additions & 22 deletions src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ public static class CommonInterest
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task.WaitAny)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter)), nameof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(TaskAwaiter)), nameof(TaskAwaiter.GetResult)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(TaskAwaiter) + "`1"), nameof(TaskAwaiter.GetResult)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ValueTaskAwaiter)), nameof(ValueTaskAwaiter.GetResult)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ValueTaskAwaiter) + "`1"), nameof(ValueTaskAwaiter.GetResult)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemRuntimeCompilerServices, nameof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter)), nameof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter.GetResult)), null),
];

Expand All @@ -53,7 +55,9 @@ public static class CommonInterest
public static readonly ImmutableArray<SyncBlockingMethod> SyncBlockingProperties =
[
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task)), nameof(Task<int>.Result)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(Task) + "`1"), nameof(Task<int>.Result)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(ValueTask)), nameof(ValueTask<int>.Result)), null),
new SyncBlockingMethod(new QualifiedMember(new QualifiedType(Namespaces.SystemThreadingTasks, nameof(ValueTask) + "`1"), nameof(ValueTask<int>.Result)), null),
];

public static readonly IEnumerable<QualifiedMember> ThreadAffinityTestingMethods =
Expand All @@ -63,6 +67,7 @@ public static class CommonInterest

public static readonly ImmutableArray<QualifiedMember> TaskConfigureAwait = ImmutableArray.Create(
new QualifiedMember(new QualifiedType(Types.Task.Namespace, Types.Task.TypeName), nameof(Task.ConfigureAwait)),
new QualifiedMember(new QualifiedType(Types.Task.Namespace, Types.Task.TypeName + "`1"), nameof(Task.ConfigureAwait)),
new QualifiedMember(new QualifiedType(Types.AwaitExtensions.Namespace, Types.AwaitExtensions.TypeName), Types.AwaitExtensions.ConfigureAwaitRunInline));

private const RegexOptions FileNamePatternRegexOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
Expand All @@ -71,25 +76,33 @@ public static class CommonInterest

public static IEnumerable<QualifiedMember> ReadMethods(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken)
{
foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken))
foreach (SourceText text in ReadAdditionalFileTexts(analyzerOptions, fileNamePattern, cancellationToken))
{
yield return ParseAdditionalFileMethodLine(line);
bool matchAnyArity = !Contains(text, '`');
foreach (string line in ReadLinesFromAdditionalFile(text))
{
yield return ParseAdditionalFileMethodLine(line, matchAnyArity);
}
}
}

public static IEnumerable<TypeMatchSpec> ReadTypesAndMembers(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken)
{
foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken))
foreach (SourceText text in ReadAdditionalFileTexts(analyzerOptions, fileNamePattern, cancellationToken))
{
if (!CommonInterestParsing.TryParseNegatableTypeOrMemberReference(line, out bool negated, out ReadOnlyMemory<char> typeNameMemory, out string? memberNameValue))
bool matchAnyArity = !Contains(text, '`');
foreach (string line in ReadLinesFromAdditionalFile(text))
{
throw new InvalidOperationException($"Parsing error on line: {line}");
}
if (!CommonInterestParsing.TryParseNegatableTypeOrMemberReference(line, out bool negated, out ReadOnlyMemory<char> typeNameMemory, out string? memberNameValue))
{
throw new InvalidOperationException($"Parsing error on line: {line}");
}

(ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory);
var type = new QualifiedType(containingNamespace, typeName);
QualifiedMember member = memberNameValue is not null ? new QualifiedMember(type, memberNameValue) : default(QualifiedMember);
yield return new TypeMatchSpec(type, member, negated);
(ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory);
var type = new QualifiedType(containingNamespace, typeName, matchAnyArity);
QualifiedMember member = memberNameValue is not null ? new QualifiedMember(type, memberNameValue) : default(QualifiedMember);
yield return new TypeMatchSpec(type, member, negated);
}
}
}

Expand All @@ -105,12 +118,7 @@ public static IEnumerable<string> ReadAdditionalFiles(AnalyzerOptions analyzerOp
throw new ArgumentNullException(nameof(fileNamePattern));
}

IEnumerable<SourceText>? docs = from file in analyzerOptions.AdditionalFiles.OrderBy(x => x.Path, StringComparer.Ordinal)
let fileName = Path.GetFileName(file.Path)
where fileNamePattern.IsMatch(fileName)
let text = file.GetText(cancellationToken)
select text;
return docs.SelectMany(ReadLinesFromAdditionalFile);
return ReadAdditionalFileTexts(analyzerOptions, fileNamePattern, cancellationToken).SelectMany(ReadLinesFromAdditionalFile);
}

public static bool Contains(this ImmutableArray<QualifiedMember> methods, ISymbol symbol)
Expand Down Expand Up @@ -320,17 +328,44 @@ public static IEnumerable<string> ReadLinesFromAdditionalFile(SourceText text)
}

public static QualifiedMember ParseAdditionalFileMethodLine(string line)
=> ParseAdditionalFileMethodLine(line, matchAnyArity: true);

public static QualifiedMember ParseAdditionalFileMethodLine(string line, bool matchAnyArity)
{
if (!CommonInterestParsing.TryParseMemberReference(line, out ReadOnlyMemory<char> typeNameMemory, out string? memberName))
{
throw new InvalidOperationException($"Parsing error on line: {line}");
}

(ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory);
var containingType = new QualifiedType(containingNamespace, typeName);
var containingType = new QualifiedType(containingNamespace, typeName, matchAnyArity);
return new QualifiedMember(containingType, memberName!);
}

/// <summary>
/// Determines whether a character appears in source text without materializing the text as a string.
/// </summary>
/// <param name="text">The source text to search.</param>
/// <param name="value">The character to find.</param>
/// <returns><see langword="true"/> if <paramref name="value"/> appears in <paramref name="text"/>; otherwise, <see langword="false"/>.</returns>
public static bool Contains(SourceText text, char value)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}

for (int i = 0; i < text.Length; i++)
{
if (text[i] == value)
{
return true;
}
}

return false;
}

/// <summary>
/// Splits a qualified type name (e.g. <c>My.Namespace.MyType</c>) into its containing namespace
/// segments and the simple type name, without allocating an intermediate joined string.
Expand Down Expand Up @@ -364,6 +399,25 @@ private static (ImmutableArray<string> ContainingNamespace, string TypeName) Spl
return (nsBuilder.ToImmutable(), typeName);
}

private static IEnumerable<SourceText> ReadAdditionalFileTexts(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken)
{
if (analyzerOptions is null)
{
throw new ArgumentNullException(nameof(analyzerOptions));
}

if (fileNamePattern is null)
{
throw new ArgumentNullException(nameof(fileNamePattern));
}

return from file in analyzerOptions.AdditionalFiles.OrderBy(x => x.Path, StringComparer.Ordinal)
let fileName = Path.GetFileName(file.Path)
where fileNamePattern.IsMatch(fileName)
let text = file.GetText(cancellationToken) ?? throw new InvalidOperationException($"Unable to read additional file: {file.Path}")
select text;
}

private static bool TestGetAwaiterMethod(IMethodSymbol getAwaiterMethod)
{
if (getAwaiterMethod.IsExtensionMethod)
Expand Down Expand Up @@ -444,16 +498,14 @@ public bool IsMatch([NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? member
}

if (!this.IsMember
&& (this.IsWildcard || typeSymbol.Name == this.Type.Name)
&& typeSymbol.BelongsToNamespace(this.Type.Namespace))
&& ((this.IsWildcard && typeSymbol.BelongsToNamespace(this.Type.Namespace)) || this.Type.IsMatch(typeSymbol)))
{
return true;
Comment thread
AArnott marked this conversation as resolved.
}

if (this.IsMember
&& memberSymbol?.Name == this.Member.Name
&& typeSymbol.Name == this.Type.Name
&& typeSymbol.BelongsToNamespace(this.Type.Namespace))
&& this.Type.IsMatch(typeSymbol))
{
return true;
}
Expand All @@ -465,18 +517,33 @@ public bool IsMatch([NotNullWhen(true)] ITypeSymbol? typeSymbol, ISymbol? member
public readonly struct QualifiedType
{
public QualifiedType(ImmutableArray<string> containingTypeNamespace, string typeName)
: this(containingTypeNamespace, typeName, matchAnyArity: false)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="QualifiedType"/> struct.
/// </summary>
/// <param name="containingTypeNamespace">The namespace containing the type.</param>
/// <param name="typeName">The simple or metadata name of the type.</param>
/// <param name="matchAnyArity"><see langword="true"/> to match the simple name across all generic arities; otherwise, to match the metadata name exactly.</param>
internal QualifiedType(ImmutableArray<string> containingTypeNamespace, string typeName, bool matchAnyArity)
{
this.Namespace = containingTypeNamespace;
this.Name = typeName;
this.MatchAnyArity = matchAnyArity;
}

public ImmutableArray<string> Namespace { get; }

public string Name { get; }

private bool MatchAnyArity { get; }

public bool IsMatch(ISymbol symbol)
{
return symbol?.Name == this.Name
return symbol is not null
&& (this.MatchAnyArity ? symbol.Name : symbol.MetadataName) == this.Name
&& symbol.BelongsToNamespace(this.Namespace);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file has no metadata arity markers, so its type names match every arity.
[TestNamespace.LegacyTestClass]::SlowSyncMethod
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Test exclusions for VSTHRD103 analyzer
[TestNamespace.TestClass]::SlowSyncMethod
[TestNamespace.TestClass]::SlowSyncMethod
[TestNamespace.GenericTestClass`1]::SlowSyncMethod
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,92 @@ public static void SlowSyncMethod() { }
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task GenericTypeExclusion_DoesNotExcludeNonGenericType()
{
string test = """
using System.Threading.Tasks;

class Test {
async Task T() {
TestNamespace.GenericTestClass<int>.SlowSyncMethod();
TestNamespace.GenericTestClass.{|#0:SlowSyncMethod|}();
}
}

namespace TestNamespace {
class GenericTestClass {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}

class GenericTestClass<T> {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}
}
""";

await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("SlowSyncMethod", "SlowSyncMethodAsync"));
}

[Fact]
public async Task NonGenericTypeExclusion_DoesNotExcludeGenericType()
{
string test = """
using System.Threading.Tasks;

class Test {
async Task T() {
TestNamespace.TestClass<int>.{|#0:SlowSyncMethod|}();
}
}

namespace TestNamespace {
class TestClass {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}

class TestClass<T> {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}
}
""";

await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("SlowSyncMethod", "SlowSyncMethodAsync"));
}

[Fact]
public async Task LegacyFileExclusion_MatchesAllArities()
{
string test = """
using System.Threading.Tasks;

class Test {
async Task T() {
TestNamespace.LegacyTestClass.SlowSyncMethod();
TestNamespace.LegacyTestClass<int>.SlowSyncMethod();
}
}

namespace TestNamespace {
class LegacyTestClass {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}

class LegacyTestClass<T> {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task SyncMethodCallInAsyncMethod_NotExcludedViaAdditionalFiles_GeneratesWarning()
{
Expand Down
Loading