diff --git a/README.md b/README.md
index f5e294f84..15bf8b1cc 100755
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0057](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0057.md)|Naming|Class name should end with 'Attribute'|ℹ️|✔️|✔️|❌|
|[MA0058](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0058.md)|Naming|Class name should end with 'Exception'|ℹ️|✔️|✔️|❌|
|[MA0059](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0059.md)|Naming|Class name should end with 'EventArgs'|ℹ️|✔️|✔️|❌|
-|[MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md)|Design|The value returned by Stream.Read/Stream.ReadAsync is not used|⚠️|✔️|❌|❌|
+|[MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md)|Design|The return value of the method should be used|⚠️|✔️|❌|✔️|
|[MA0061](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0061.md)|Design|Method overrides should not change default values|⚠️|✔️|✔️|❌|
|[MA0062](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0062.md)|Design|Non-flags enums should not be marked with "FlagsAttribute"|⚠️|✔️|✔️|✔️|
|[MA0063](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0063.md)|Performance|Use Where before OrderBy|ℹ️|✔️|✔️|❌|
diff --git a/docs/README.md b/docs/README.md
index a5af1fa94..466b0e2bc 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -59,7 +59,7 @@
|[MA0057](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0057.md)|Naming|Class name should end with 'Attribute'|ℹ️|✔️|✔️|❌|
|[MA0058](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0058.md)|Naming|Class name should end with 'Exception'|ℹ️|✔️|✔️|❌|
|[MA0059](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0059.md)|Naming|Class name should end with 'EventArgs'|ℹ️|✔️|✔️|❌|
-|[MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md)|Design|The value returned by Stream.Read/Stream.ReadAsync is not used|⚠️|✔️|❌|❌|
+|[MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md)|Design|The return value of the method should be used|⚠️|✔️|❌|✔️|
|[MA0061](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0061.md)|Design|Method overrides should not change default values|⚠️|✔️|✔️|❌|
|[MA0062](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0062.md)|Design|Non-flags enums should not be marked with "FlagsAttribute"|⚠️|✔️|✔️|✔️|
|[MA0063](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0063.md)|Performance|Use Where before OrderBy|ℹ️|✔️|✔️|❌|
diff --git a/docs/Rules/MA0060.md b/docs/Rules/MA0060.md
index b5b05e087..269df3128 100644
--- a/docs/Rules/MA0060.md
+++ b/docs/Rules/MA0060.md
@@ -1,21 +1,145 @@
-# MA0060 - The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060 - The return value of the method should be used
-Source: [ValueReturnedByStreamReadShouldBeUsedAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzer.cs)
+Source: [DoNotIgnoreReturnValueAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotIgnoreReturnValueAnalyzer.cs)
-You should use the value of `Stream.Read` to know how many bytes were actually read. This can be less than the number of bytes requested, if that many bytes are not currently available, or zero if the end of the stream was reached.
+The return value of certain methods must not be ignored because ignoring it typically indicates a bug. Similarly, `out` parameters marked with `[DoNotIgnore]` must not be discarded with `out _`.
+
+## Built-in methods
+
+The following CLR methods are checked automatically without any annotation:
+
+| Type | Methods |
+|------|---------|
+| `System.IO.Stream` | `Read`, `ReadAsync`, `ReadAtLeast`, `ReadAtLeastAsync` |
+| `System.IO.TextReader` | `Read`, `ReadAsync` |
+| `System.IO.BinaryReader` | `Read` |
+| `System.String` | `ToUpper`, `ToLower`, `Trim`, `TrimEnd`, `TrimStart`, `ToUpperInvariant`, `ToLowerInvariant`, `Clone`, `Format`, `Concat`, `Copy`, `Insert`, `Join`, `Normalize`, `Remove`, `Replace`, `Split`, `PadLeft`, `PadRight`, `Substring` |
+| `System.Collections.Immutable.IImmutableDictionary` | `Clear`, `Add`, `AddRange`, `SetItem`, `SetItems`, `Remove`, `RemoveRange`, `Contains`, `TryGetKey` |
+| `System.Collections.Immutable.IImmutableList` | `Clear`, `Add`, `AddRange`, `Insert`, `InsertRange`, `Remove`, `RemoveAll`, `RemoveRange`, `RemoveAt`, `SetItem`, `Replace`, `IndexOf`, `LastIndexOf` |
+| `System.Collections.Immutable.IImmutableQueue` | `Clear`, `Enqueue`, `Dequeue`, `Peek` |
+| `System.Collections.Immutable.IImmutableSet` | `Clear`, `Add`, `Remove`, `Contains`, `TryGetValue`, `Intersect`, `Except`, `SymmetricExcept`, `Union`, `SetEquals`, `IsProperSubsetOf`, `IsProperSupersetOf`, `IsSubsetOf`, `IsSupersetOf`, `Overlaps` |
+| `System.Collections.Immutable.IImmutableStack` | `Clear`, `Push`, `Pop`, `Peek` |
+| `System.Collections.Immutable.ImmutableArray` (static) | `Create`, `CreateRange`, `CreateBuilder`, `ToImmutableArray`, `BinarySearch` |
+| `System.Collections.Immutable.ImmutableArray.Builder` | `IndexOf`, `LastIndexOf` |
+| `Windows.Win32.Foundation.HRESULT` (generated by [CsWin32](https://github.com/microsoft/CsWin32)) | any method returning `HRESULT` |
+
+In addition, any method whose name starts with `TryParse`, returns `bool`, and has at least one `out`/`ref` parameter is also checked.
+
+This `TryParse` pattern detection can be disabled with:
+
+```ini
+dotnet_diagnostic.MA0060.enable_tryparse_pattern = false
+```
+
+## [Pure] attribute
+
+Methods decorated with `[System.Diagnostics.Contracts.Pure]` or `[JetBrains.Annotations.Pure]` have their return value checked automatically.
+
+```csharp
+using System.Diagnostics.Contracts;
+
+class MyClass
+{
+ [Pure]
+ public int Compute() => 42;
+}
+
+class Test
+{
+ void A(MyClass obj)
+ {
+ obj.Compute(); // Non-compliant: return value ignored
+ var x = obj.Compute(); // Compliant
+ }
+}
+```
+
+## Custom methods via attribute
+
+Use the `[DoNotIgnore]` attribute from `Meziantou.Analyzer.Annotations` to annotate return values or `out` parameters of custom methods. You can also declare it at the assembly level with an XML documentation ID to mark methods you cannot modify directly.
+
+```csharp
+// Add the NuGet package: Meziantou.Analyzer.Annotations
+using Meziantou.Analyzer.Annotations;
+
+class MyClass
+{
+ // Annotate the return value
+ [return: DoNotIgnore(Message = "Use the result to check whether the operation succeeded")]
+ public bool TrySave() { ... }
+
+ // Annotate an out parameter – using out _ is a diagnostic
+ public bool TryGetValue([DoNotIgnore] out int value) { ... }
+}
+```
+
+```csharp
+using Meziantou.Analyzer.Annotations;
+
+[assembly: DoNotIgnore("M:NativeMethods.Check")]
+
+static class NativeMethods
+{
+ public static int Check() => 0;
+}
+```
+
+Assembly-level annotations support XML documentation IDs, including nested types, generic types, and generic methods.
+
+## Examples
````csharp
+using System.IO;
+
class Test
{
void A()
{
var stream = File.OpenRead("file.txt");
var bytes = new byte[10];
- stream.Read(bytes, 0, bytes.Length); // Non-compliant
- var read = stream.Read(bytes, 0, bytes.Length); // ok
+ stream.Read(bytes, 0, bytes.Length); // Non-compliant: return value ignored
+
+ var read = stream.Read(bytes, 0, bytes.Length); // Compliant
var data = bytes.AsSpan(0, read);
}
}
````
+
+````csharp
+using Meziantou.Analyzer.Annotations;
+
+[assembly: DoNotIgnore("M:Test.Compute")]
+
+class Test
+{
+ static int Compute() => 42;
+
+ void A()
+ {
+ Compute(); // Non-compliant: return value ignored
+ }
+}
+````
+
+````csharp
+using Meziantou.Analyzer.Annotations;
+
+class Test
+{
+ [return: DoNotIgnore]
+ static int Compute() => 42;
+
+ static bool TryGet([DoNotIgnore] out int value) { value = 0; return true; }
+
+ void A()
+ {
+ Compute(); // Non-compliant: return value ignored
+ var x = Compute(); // Compliant
+
+ TryGet(out _); // Non-compliant: out parameter discarded
+ TryGet(out int v); // Compliant
+ }
+}
+````
diff --git a/src/Meziantou.Analyzer.Annotations/DoNotIgnoreAttribute.cs b/src/Meziantou.Analyzer.Annotations/DoNotIgnoreAttribute.cs
new file mode 100644
index 000000000..0e2efc5ae
--- /dev/null
+++ b/src/Meziantou.Analyzer.Annotations/DoNotIgnoreAttribute.cs
@@ -0,0 +1,25 @@
+#pragma warning disable CS1591
+#pragma warning disable IDE0060
+
+namespace Meziantou.Analyzer.Annotations;
+
+///
+/// Indicates that the return value or the value of an parameter must not be ignored.
+///
+[System.Diagnostics.Conditional("MEZIANTOU_ANALYZER_ANNOTATIONS")]
+[System.AttributeUsage(System.AttributeTargets.ReturnValue | System.AttributeTargets.Parameter | System.AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
+public sealed class DoNotIgnoreAttribute : System.Attribute
+{
+ public DoNotIgnoreAttribute() { }
+
+ public DoNotIgnoreAttribute(string xmlDocumentationId)
+ {
+ XmlDocumentationId = xmlDocumentationId;
+ }
+
+ /// Gets the XML documentation id of a method annotated at assembly level.
+ public string? XmlDocumentationId { get; }
+
+ /// Gets or sets an optional message explaining why the value must not be ignored.
+ public string? Message { get; set; }
+}
diff --git a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj
index 5b3a0a455..be790db33 100644
--- a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj
+++ b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj
@@ -2,7 +2,7 @@
netstandard2.0
- 1.5.0
+ 1.6.0
Annotations to configure Meziantou.Analyzer
Meziantou.Analyzer, analyzers
True
diff --git a/src/Meziantou.Analyzer.Annotations/README.md b/src/Meziantou.Analyzer.Annotations/README.md
index e61d15754..526134655 100644
--- a/src/Meziantou.Analyzer.Annotations/README.md
+++ b/src/Meziantou.Analyzer.Annotations/README.md
@@ -18,6 +18,7 @@ If you want to keep these attributes in the metadata (for example, for reflectio
| Attribute | Purpose | Related rules |
| --- | --- | --- |
+| `DoNotIgnoreAttribute` | Marks a return value or `out` parameter as must-not-be-ignored. | [MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md) |
| `CultureInsensitiveTypeAttribute` | Marks a type (or a specific format) as culture-insensitive. | [MA0011](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0011.md), [MA0075](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0075.md), [MA0076](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0076.md), [MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md) |
| `NonAwaitableTypeAttribute` | Excludes await recommendations for specific types. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md), [MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md), [MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md), [MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md) |
| `NonAsyncDisposableTypeAttribute` | Excludes `await using` recommendations for specific types. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md) |
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
index d88af3b88..b935877ad 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
@@ -176,7 +176,7 @@ dotnet_diagnostic.MA0058.severity = error
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = error
-# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060: The return value of the method should be used
dotnet_diagnostic.MA0060.severity = error
# MA0061: Method overrides should not change default values
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
index 272d77932..fd332159b 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
@@ -176,7 +176,7 @@ dotnet_diagnostic.MA0058.severity = suggestion
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = suggestion
-# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060: The return value of the method should be used
dotnet_diagnostic.MA0060.severity = suggestion
# MA0061: Method overrides should not change default values
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
index 14e4cd4cb..24bc5b38c 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
@@ -176,7 +176,7 @@ dotnet_diagnostic.MA0058.severity = warning
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = warning
-# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060: The return value of the method should be used
dotnet_diagnostic.MA0060.severity = warning
# MA0061: Method overrides should not change default values
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index c7d79bfbc..8dfcc4626 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -176,7 +176,7 @@ dotnet_diagnostic.MA0058.severity = suggestion
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = suggestion
-# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060: The return value of the method should be used
dotnet_diagnostic.MA0060.severity = warning
# MA0061: Method overrides should not change default values
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 09dcdb124..8efbe6466 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -176,7 +176,7 @@ dotnet_diagnostic.MA0058.severity = none
# MA0059: Class name should end with 'EventArgs'
dotnet_diagnostic.MA0059.severity = none
-# MA0060: The value returned by Stream.Read/Stream.ReadAsync is not used
+# MA0060: The return value of the method should be used
dotnet_diagnostic.MA0060.severity = none
# MA0061: Method overrides should not change default values
diff --git a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs
index ad30dc135..9172a9eea 100644
--- a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs
+++ b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs
@@ -114,4 +114,26 @@ public static bool IsNonAsyncDisposableTypeAttributeSymbol(ITypeSymbol? symbol)
}
};
}
+
+ public static bool IsDoNotIgnoreAttributeSymbol(ITypeSymbol? symbol)
+ {
+ // Meziantou.Analyzer.Annotations.DoNotIgnoreAttribute
+ return symbol is INamedTypeSymbol
+ {
+ Name: "DoNotIgnoreAttribute",
+ ContainingSymbol: INamespaceSymbol
+ {
+ Name: "Annotations",
+ ContainingSymbol: INamespaceSymbol
+ {
+ Name: "Analyzer",
+ ContainingSymbol: INamespaceSymbol
+ {
+ Name: "Meziantou",
+ ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true }
+ }
+ }
+ }
+ };
+ }
}
diff --git a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs
index f75bdf551..926eb9b66 100755
--- a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs
+++ b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs
@@ -177,6 +177,39 @@ public static bool HasAttribute(this ISymbol symbol, [NotNullWhen(true)] ITypeSy
return GetAttribute(symbol, attributeType, inherits) is not null;
}
+ public static AttributeData? GetReturnTypeAttribute(this IMethodSymbol method, ITypeSymbol? attributeType, bool inherits = true)
+ {
+ if (attributeType is null)
+ return null;
+
+ if (attributeType.IsSealed)
+ inherits = false;
+
+ foreach (var attribute in method.GetReturnTypeAttributes())
+ {
+ if (attribute.AttributeClass is null)
+ continue;
+
+ if (inherits)
+ {
+ if (attribute.AttributeClass.IsOrInheritFrom(attributeType))
+ return attribute;
+ }
+ else
+ {
+ if (attributeType.IsEqualTo(attribute.AttributeClass))
+ return attribute;
+ }
+ }
+
+ return null;
+ }
+
+ public static bool HasReturnTypeAttribute(this IMethodSymbol method, [NotNullWhen(true)] ITypeSymbol? attributeType, bool inherits = true)
+ {
+ return GetReturnTypeAttribute(method, attributeType, inherits) is not null;
+ }
+
public static bool IsOrInheritFrom(this ITypeSymbol symbol, [NotNullWhen(true)] ITypeSymbol? expectedType)
{
return IsOrInheritFrom(symbol, expectedType, visitedTypeParameters: null);
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index cfb565dcc..cd86fd749 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -60,7 +60,7 @@ internal static class RuleIdentifiers
public const string AttributeNameShouldEndWithAttribute = "MA0057";
public const string ExceptionNameShouldEndWithException = "MA0058";
public const string EventArgsNameShouldEndWithEventArgs = "MA0059";
- public const string TheReturnValueOfStreamReadShouldBeUsed = "MA0060";
+ public const string DoNotIgnoreReturnValue = "MA0060";
public const string MethodOverridesShouldNotChangeParameterDefaults = "MA0061";
public const string NonFlagsEnumsShouldNotBeMarkedWithFlagsAttribute = "MA0062";
public const string OptimizeEnumerable_WhereBeforeOrderBy = "MA0063";
diff --git a/src/Meziantou.Analyzer/Rules/DoNotIgnoreReturnValueAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotIgnoreReturnValueAnalyzer.cs
new file mode 100644
index 000000000..69b33e492
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/DoNotIgnoreReturnValueAnalyzer.cs
@@ -0,0 +1,297 @@
+using System.Collections.Immutable;
+using Meziantou.Analyzer.Configurations;
+using Meziantou.Analyzer.Internals;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class DoNotIgnoreReturnValueAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly ConfigurationDefinition EnableTryParsePatternConfiguration = new(RuleIdentifiers.DoNotIgnoreReturnValue + ".enable_tryparse_pattern", defaultValue: true);
+
+ private static readonly DiagnosticDescriptor ReturnValueRule = new(
+ RuleIdentifiers.DoNotIgnoreReturnValue,
+ title: "The return value of the method should be used",
+ messageFormat: "The return value of '{0}' should be used{1}",
+ RuleCategories.Design,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotIgnoreReturnValue));
+
+ private static readonly DiagnosticDescriptor OutParameterRule = new(
+ RuleIdentifiers.DoNotIgnoreReturnValue,
+ title: "The return value of the method should be used",
+ messageFormat: "The out parameter '{0}' of '{1}' should not be discarded{2}",
+ RuleCategories.Design,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotIgnoreReturnValue));
+
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(ReturnValueRule, OutParameterRule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+ context.RegisterCompilationStartAction(compilationContext =>
+ {
+ var analyzerContext = new AnalyzerContext(compilationContext.Compilation, compilationContext.Options);
+ compilationContext.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ compilationContext.RegisterOperationAction(analyzerContext.AnalyzeArgument, OperationKind.Argument);
+ });
+ }
+
+ private sealed class AnalyzerContext(Compilation compilation, AnalyzerOptions options)
+ {
+ private INamedTypeSymbol? DoNotIgnoreAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.DoNotIgnoreAttribute");
+ private INamedTypeSymbol? SystemDiagnosticsContractsPureAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Diagnostics.Contracts.PureAttribute");
+ private INamedTypeSymbol? JetBrainsAnnotationsPureAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("JetBrains.Annotations.PureAttribute");
+ private INamedTypeSymbol? StreamSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IO.Stream");
+ private INamedTypeSymbol? TextReaderSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IO.TextReader");
+ private INamedTypeSymbol? BinaryReaderSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IO.BinaryReader");
+ private INamedTypeSymbol? StringSymbol { get; } = compilation.GetSpecialType(SpecialType.System_String);
+ private INamedTypeSymbol? IImmutableDictionarySymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableDictionary`2");
+ private INamedTypeSymbol? IImmutableListSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableList`1");
+ private INamedTypeSymbol? IImmutableQueueSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableQueue`1");
+ private INamedTypeSymbol? IImmutableSetSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableSet`1");
+ private INamedTypeSymbol? IImmutableStackSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableStack`1");
+ private INamedTypeSymbol? ImmutableArraySymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.ImmutableArray");
+ private INamedTypeSymbol? ImmutableArrayBuilderSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.ImmutableArray`1+Builder")
+ ?? compilation.GetBestTypeByMetadataName("System.Collections.Immutable.ImmutableArray`1.Builder");
+
+ // Generated by CsWin32 (https://github.com/microsoft/CsWin32)
+ private INamedTypeSymbol? HResultSymbol { get; } = compilation.GetBestTypeByMetadataName("Windows.Win32.Foundation.HRESULT");
+ private ImmutableHashSet AssemblyLevelDoNotIgnoreSymbols { get; } = GetAssemblyLevelDoNotIgnoreSymbols(compilation);
+ private AnalyzerOptions Options { get; } = options;
+
+ public void AnalyzeArgument(OperationAnalysisContext context)
+ {
+ if (DoNotIgnoreAttributeSymbol is null)
+ return;
+
+ var argument = (IArgumentOperation)context.Operation;
+ if (argument.Parameter is not { RefKind: RefKind.Out } outParam)
+ return;
+
+ if (argument.Value is not IDiscardOperation)
+ return;
+
+ if (!outParam.HasAttribute(DoNotIgnoreAttributeSymbol))
+ return;
+
+ var methodName = argument.Parent is IInvocationOperation inv ? inv.TargetMethod.Name : "?";
+ var attr = outParam.GetAttribute(DoNotIgnoreAttributeSymbol);
+ var message = attr is not null ? GetMessageFromAttributeData(attr) : null;
+ context.ReportDiagnostic(OutParameterRule, argument,
+ outParam.Name, methodName, message is null ? "" : ": " + message);
+ }
+
+ public void AnalyzeInvocation(OperationAnalysisContext context)
+ {
+ var invocation = (IInvocationOperation)context.Operation;
+ var targetMethod = invocation.TargetMethod;
+
+ if (targetMethod.ReturnsVoid)
+ return;
+
+ // Check return value
+ if (!IsReturnValueIgnored(invocation))
+ return;
+
+ // Check attribute on return value
+ if (DoNotIgnoreAttributeSymbol is not null)
+ {
+ var attr = targetMethod.GetReturnTypeAttribute(DoNotIgnoreAttributeSymbol);
+ if (attr is not null)
+ {
+ var message = GetMessageFromAttributeData(attr);
+ context.ReportDiagnostic(ReturnValueRule, invocation,
+ targetMethod.Name, message is null ? "" : ": " + message);
+ return;
+ }
+ }
+
+ if (AssemblyLevelDoNotIgnoreSymbols.Contains(targetMethod.OriginalDefinition))
+ {
+ context.ReportDiagnostic(ReturnValueRule, invocation, targetMethod.Name, "");
+ return;
+ }
+
+ // Check [Pure] attribute on the method
+ if ((SystemDiagnosticsContractsPureAttributeSymbol is not null && targetMethod.HasAttribute(SystemDiagnosticsContractsPureAttributeSymbol)) ||
+ (JetBrainsAnnotationsPureAttributeSymbol is not null && targetMethod.HasAttribute(JetBrainsAnnotationsPureAttributeSymbol)))
+ {
+ context.ReportDiagnostic(ReturnValueRule, invocation, targetMethod.Name, "");
+ return;
+ }
+
+ // Check HRESULT return type (generated by CsWin32)
+ if (HResultSymbol is not null && targetMethod.ReturnType.IsEqualTo(HResultSymbol))
+ {
+ context.ReportDiagnostic(ReturnValueRule, invocation, targetMethod.Name, "");
+ return;
+ }
+
+ // Check built-in CLR list
+ if (IsBuiltInMethod(context, targetMethod))
+ {
+ context.ReportDiagnostic(ReturnValueRule, invocation, targetMethod.Name, "");
+ }
+ }
+
+ private static bool IsReturnValueIgnored(IInvocationOperation invocation)
+ {
+ var parent = invocation.Parent;
+ if (parent is IAwaitOperation)
+ {
+ parent = parent.Parent;
+ }
+
+ return parent is null or IBlockOperation or IExpressionStatementOperation;
+ }
+
+ private bool IsBuiltInMethod(OperationAnalysisContext context, IMethodSymbol method)
+ {
+ var containingType = method.ContainingType;
+
+ if (StreamSymbol is not null && containingType.IsOrInheritFrom(StreamSymbol))
+ {
+ return method.Name is
+ nameof(System.IO.Stream.Read) or
+ "ReadAsync" or
+ "ReadAtLeast" or
+ "ReadAtLeastAsync";
+ }
+
+ if (TextReaderSymbol is not null && containingType.IsOrInheritFrom(TextReaderSymbol))
+ {
+ return method.Name is
+ nameof(System.IO.TextReader.Read) or
+ "ReadAsync";
+ }
+
+ if (BinaryReaderSymbol is not null && containingType.IsOrInheritFrom(BinaryReaderSymbol))
+ {
+ return method.Name is nameof(System.IO.BinaryReader.Read);
+ }
+
+ if (StringSymbol is not null && containingType.IsEqualTo(StringSymbol))
+ {
+ return method.Name is
+ nameof(string.ToUpper) or
+ nameof(string.ToLower) or
+ nameof(string.Trim) or
+ nameof(string.TrimEnd) or
+ nameof(string.TrimStart) or
+ nameof(string.ToUpperInvariant) or
+ nameof(string.ToLowerInvariant) or
+ nameof(string.Clone) or
+ nameof(string.Format) or
+ nameof(string.Concat) or
+ nameof(string.Copy) or
+ nameof(string.Insert) or
+ nameof(string.Join) or
+ nameof(string.Normalize) or
+ nameof(string.Remove) or
+ nameof(string.Replace) or
+ nameof(string.Split) or
+ nameof(string.PadLeft) or
+ nameof(string.PadRight) or
+ nameof(string.Substring);
+ }
+
+ if (IImmutableDictionarySymbol is not null && (containingType.ImplementsGenericInterface(IImmutableDictionarySymbol) || containingType.OriginalDefinition.IsEqualTo(IImmutableDictionarySymbol)))
+ {
+ return method.Name is "Clear" or "Add" or "AddRange" or "SetItem" or "SetItems" or "RemoveRange" or "Remove" or "Contains" or "TryGetKey";
+ }
+
+ if (IImmutableListSymbol is not null && (containingType.ImplementsGenericInterface(IImmutableListSymbol) || containingType.OriginalDefinition.IsEqualTo(IImmutableListSymbol)))
+ {
+ return method.Name is "Clear" or "IndexOf" or "LastIndexOf" or "Add" or "AddRange" or "Insert" or "InsertRange" or "Remove" or "RemoveAll" or "RemoveRange" or "RemoveAt" or "SetItem" or "Replace";
+ }
+
+ if (IImmutableQueueSymbol is not null && (containingType.ImplementsGenericInterface(IImmutableQueueSymbol) || containingType.OriginalDefinition.IsEqualTo(IImmutableQueueSymbol)))
+ {
+ return method.Name is "Clear" or "Peek" or "Enqueue" or "Dequeue";
+ }
+
+ if (IImmutableSetSymbol is not null && (containingType.ImplementsGenericInterface(IImmutableSetSymbol) || containingType.OriginalDefinition.IsEqualTo(IImmutableSetSymbol)))
+ {
+ return method.Name is "Clear" or "Contains" or "Add" or "Remove" or "TryGetValue" or "Intersect" or "Except" or "SymmetricExcept" or "Union" or "SetEquals" or "IsProperSubsetOf" or "IsProperSupersetOf" or "IsSubsetOf" or "IsSupersetOf" or "Overlaps";
+ }
+
+ if (IImmutableStackSymbol is not null && (containingType.ImplementsGenericInterface(IImmutableStackSymbol) || containingType.OriginalDefinition.IsEqualTo(IImmutableStackSymbol)))
+ {
+ return method.Name is "Clear" or "Push" or "Pop" or "Peek";
+ }
+
+ if (ImmutableArraySymbol is not null && containingType.IsEqualTo(ImmutableArraySymbol))
+ {
+ return method.Name is "Create" or "CreateRange" or "CreateBuilder" or "ToImmutableArray" or "BinarySearch";
+ }
+
+ if (ImmutableArrayBuilderSymbol is not null && containingType.OriginalDefinition.IsEqualTo(ImmutableArrayBuilderSymbol))
+ {
+ return method.Name is "IndexOf" or "LastIndexOf";
+ }
+
+ return IsTryParseMethodEnabled(context) && IsTryParseMethod(method);
+ }
+
+ private bool IsTryParseMethodEnabled(OperationAnalysisContext context)
+ {
+ return Options.GetConfigurationValue(context.Operation, EnableTryParsePatternConfiguration);
+ }
+
+ private static bool IsTryParseMethod(IMethodSymbol method)
+ {
+ return method.Name.StartsWith("TryParse", StringComparison.Ordinal) &&
+ method.ReturnType.SpecialType == SpecialType.System_Boolean &&
+ method.Parameters.Length >= 2 &&
+ method.Parameters[method.Parameters.Length - 1].RefKind != RefKind.None;
+ }
+
+ private static string? GetMessageFromAttributeData(AttributeData attr)
+ {
+ foreach (var namedArg in attr.NamedArguments)
+ {
+ if (namedArg.Key == "Message" && namedArg.Value.Value is string msg)
+ return msg;
+ }
+
+ return null;
+ }
+
+ private static ImmutableHashSet GetAssemblyLevelDoNotIgnoreSymbols(Compilation compilation)
+ {
+ var builder = ImmutableHashSet.CreateBuilder(SymbolEqualityComparer.Default);
+ foreach (var attr in compilation.Assembly.GetAttributes())
+ {
+ if (!AnnotationAttributes.IsDoNotIgnoreAttributeSymbol(attr.AttributeClass))
+ continue;
+
+ if (attr.ConstructorArguments.Length != 1 || attr.ConstructorArguments[0].Kind != TypedConstantKind.Primitive || attr.ConstructorArguments[0].Value is not string value)
+ continue;
+
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(value, compilation);
+ if (symbol is not null)
+ {
+ builder.Add(symbol);
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+ }
+}
diff --git a/src/Meziantou.Analyzer/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzer.cs
deleted file mode 100644
index 359d9a27e..000000000
--- a/src/Meziantou.Analyzer/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzer.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System.Collections.Immutable;
-using Meziantou.Analyzer.Internals;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis.Operations;
-
-namespace Meziantou.Analyzer.Rules;
-
-[DiagnosticAnalyzer(LanguageNames.CSharp)]
-public sealed class ValueReturnedByStreamReadShouldBeUsedAnalyzer : DiagnosticAnalyzer
-{
- private static readonly DiagnosticDescriptor Rule = new(
- RuleIdentifiers.TheReturnValueOfStreamReadShouldBeUsed,
- title: "The value returned by Stream.Read/Stream.ReadAsync is not used",
- messageFormat: "The value returned by '{0}' is not used",
- RuleCategories.Design,
- DiagnosticSeverity.Warning,
- isEnabledByDefault: true,
- description: "",
- helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.TheReturnValueOfStreamReadShouldBeUsed));
-
- public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
-
- public override void Initialize(AnalysisContext context)
- {
- context.EnableConcurrentExecution();
- context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
-
- context.RegisterCompilationStartAction(context =>
- {
- var streamSymbol = context.Compilation.GetBestTypeByMetadataName("System.IO.Stream");
- if (streamSymbol is null)
- return;
-
- context.RegisterOperationAction(context => AnalyzeOperation(context, streamSymbol), OperationKind.Invocation);
- });
- }
-
- private static void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol streamSymbol)
- {
- var invocation = (IInvocationOperation)context.Operation;
- var targetMethod = invocation.TargetMethod;
- if (targetMethod.Name is not nameof(Stream.Read) and not nameof(Stream.ReadAsync))
- return;
-
- if (!targetMethod.ContainingType.IsOrInheritFrom(streamSymbol))
- return;
-
- var parent = invocation.Parent;
- if (parent is IAwaitOperation)
- {
- parent = parent.Parent;
- }
-
- if (parent is null or IBlockOperation or IExpressionStatementOperation)
- {
- context.ReportDiagnostic(Rule, invocation, targetMethod.Name);
- }
- }
-}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotIgnoreReturnValueAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotIgnoreReturnValueAnalyzerTests.cs
new file mode 100644
index 000000000..7909c6005
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotIgnoreReturnValueAnalyzerTests.cs
@@ -0,0 +1,913 @@
+using Meziantou.Analyzer.Rules;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class DoNotIgnoreReturnValueAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .AddMeziantouAttributes();
+ }
+
+ [Fact]
+ public async Task Stream_Read_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A()
+ {
+ var stream = File.OpenRead("");
+ [|stream.Read(null, 0, 0)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Stream_ReadAsync_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ async void A()
+ {
+ var stream = File.OpenRead("");
+ await [|stream.ReadAsync(null, 0, 0)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Stream_ReadAsync_ReturnValueUsed_DiscardOperator()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ async void A()
+ {
+ var stream = File.OpenRead("");
+ _ = await stream.ReadAsync(null, 0, 0);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Stream_Read_ReturnValueUsed_MethodCall()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A()
+ {
+ var stream = File.OpenRead("");
+ System.Console.Write(stream.Read(null, 0, 0));
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Stream_ReadByte_ReturnValueNotUsed_NoDiagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A()
+ {
+ var stream = File.OpenRead("");
+ stream.ReadByte();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TextReader_ReadLine_ReturnValueNotUsed_NoDiagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A()
+ {
+ var reader = new StringReader("test");
+ reader.ReadLine();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TextReader_ReadLine_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A()
+ {
+ var reader = new StringReader("test");
+ var line = reader.ReadLine();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task BinaryReader_ReadInt32_ReturnValueNotUsed_NoDiagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A(BinaryReader reader)
+ {
+ reader.ReadInt32();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Attribute_ReturnValue_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using Meziantou.Analyzer.Annotations;
+ class Test
+ {
+ [return: DoNotIgnore]
+ static int Compute() => 0;
+
+ void A()
+ {
+ [|Compute()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Attribute_ReturnValue_Used()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using Meziantou.Analyzer.Annotations;
+ class Test
+ {
+ [return: DoNotIgnore]
+ static int Compute() => 0;
+
+ void A()
+ {
+ var result = Compute();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Attribute_ReturnValue_WithMessage()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using Meziantou.Analyzer.Annotations;
+ class Test
+ {
+ [return: DoNotIgnore(Message = "Use the result to check success")]
+ static int Compute() => 0;
+
+ void A()
+ {
+ [|Compute()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Attribute_OutParameter_Discarded()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using Meziantou.Analyzer.Annotations;
+ class Test
+ {
+ static bool TryGet([DoNotIgnore] out int value) { value = 0; return true; }
+
+ void A()
+ {
+ TryGet({|MA0060:out _|});
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Attribute_OutParameter_NotDiscarded()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using Meziantou.Analyzer.Annotations;
+ class Test
+ {
+ static bool TryGet([DoNotIgnore] out int value) { value = 0; return true; }
+
+ void A()
+ {
+ TryGet(out int x);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Pure_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Diagnostics.Contracts;
+ class Test
+ {
+ [Pure]
+ static int Compute() => 0;
+
+ void A()
+ {
+ [|Compute()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Pure_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Diagnostics.Contracts;
+ class Test
+ {
+ [Pure]
+ static int Compute() => 0;
+
+ void A()
+ {
+ var result = Compute();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Pure_OnClass_NoMethodDiagnostic()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Diagnostics.Contracts;
+ [Pure]
+ class MyClass
+ {
+ public int Compute() => 0;
+ }
+ class Test
+ {
+ void A(MyClass obj)
+ {
+ obj.Compute();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Pure_JetBrainsAttribute_IsAlsoSupported_WhenSystemDiagnosticsContractsPureExists()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Diagnostics.Contracts;
+
+ namespace JetBrains.Annotations
+ {
+ [System.AttributeUsage(System.AttributeTargets.Method)]
+ sealed class PureAttribute : System.Attribute
+ {
+ }
+ }
+
+ class Test
+ {
+ [JetBrains.Annotations.Pure]
+ static int Compute() => 0;
+
+ void A()
+ {
+ [|Compute()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Trim_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A(string s)
+ {
+ [|s.Trim()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Trim_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A(string s)
+ {
+ var trimmed = s.Trim();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Replace_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A(string s)
+ {
+ [|s.Replace("a", "b")|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Format_ArrowVoidMethod_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A() => [|string.Format("{0}", 1)|];
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Format_ArrowStringMethod_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ string A() => string.Format("{0}", 1);
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TryParse_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ [|int.TryParse("42", out _)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TryParse_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ if (int.TryParse("42", out var value)) { }
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TryParse_CustomMethod_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ static bool TryParseItem(string s, out int result) { result = 0; return true; }
+
+ void A()
+ {
+ [|TryParseItem("42", out _)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TryParse_ReturnValueNotUsed_DisabledUsingConfiguration()
+ {
+ await CreateProjectBuilder()
+ .WithAnalyzerConfiguration(new Dictionary
+ {
+ ["MA0060.enable_tryparse_pattern"] = "false",
+ })
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ int.TryParse("42", out _);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TryParse_ReturnValueNotUsed_InvalidConfiguration_UsesDefaultValue()
+ {
+ await CreateProjectBuilder()
+ .WithAnalyzerConfiguration(new Dictionary
+ {
+ ["MA0060.enable_tryparse_pattern"] = "invalid",
+ })
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ [|int.TryParse("42", out _)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableList_Add_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableList list)
+ {
+ [|list.Add(1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableList_Add_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableList list)
+ {
+ var newList = list.Add(1);
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableDictionary_Remove_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableDictionary dict)
+ {
+ [|dict.Remove("key")|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableStack_Push_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableStack stack)
+ {
+ [|stack.Push(1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableArray_Create_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A()
+ {
+ [|ImmutableArray.Create(1, 2, 3)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task Stream_ReadAtLeast_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A(Stream stream, byte[] buffer)
+ {
+ [|stream.ReadAtLeast(buffer, 1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TextReader_Read_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A(TextReader reader)
+ {
+ [|reader.Read()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task BinaryReader_Read_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.IO;
+ class Test
+ {
+ void A(BinaryReader reader)
+ {
+ [|reader.Read()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_ToUpper_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A(string s)
+ {
+ [|s.ToUpper()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task String_Join_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ [|string.Join(", ", "a", "b")|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableDictionary_Add_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableDictionary dict)
+ {
+ [|dict.Add("key", 1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableQueue_Enqueue_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableQueue queue)
+ {
+ [|queue.Enqueue(1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableSet_Add_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableHashSet set)
+ {
+ [|set.Add(1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task ImmutableArrayBuilder_IndexOf_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ using System.Collections.Immutable;
+ class Test
+ {
+ void A(ImmutableArray.Builder builder)
+ {
+ [|builder.IndexOf(1)|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task HResult_ReturnValueNotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ [|NativeMethod()|];
+ }
+
+ Windows.Win32.Foundation.HRESULT NativeMethod() => default;
+ }
+
+ namespace Windows.Win32.Foundation
+ {
+ public struct HRESULT { }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task HResult_ReturnValueUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ class Test
+ {
+ void A()
+ {
+ var hr = NativeMethod();
+ }
+
+ Windows.Win32.Foundation.HRESULT NativeMethod() => default;
+ }
+
+ namespace Windows.Win32.Foundation
+ {
+ public struct HRESULT { }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_SimpleMethod_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.Sample")]
+ class Test
+ {
+ static int Sample() => 42;
+
+ void A()
+ {
+ [|Sample()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_SimpleMethod_Used()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.Sample")]
+ class Test
+ {
+ static int Sample() => 42;
+
+ void A()
+ {
+ var value = Sample();
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_NestedTypeMethod_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.Nested.Sample")]
+ class Test
+ {
+ class Nested
+ {
+ public static int Sample() => 42;
+ }
+
+ void A()
+ {
+ [|Nested.Sample()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_GenericTypeMethod_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test`1.Sample")]
+ class Test
+ {
+ static int Sample() => 42;
+
+ void A()
+ {
+ [|Sample()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_GenericMethod_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.Sample``1")]
+ class Test
+ {
+ static int Sample() => 42;
+
+ void A()
+ {
+ [|Sample()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task AssemblyAttribute_MultipleEntries_NotUsed()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.SampleA")]
+ [assembly: Meziantou.Analyzer.Annotations.DoNotIgnore("M:Test.SampleB")]
+ class Test
+ {
+ static int SampleA() => 1;
+ static int SampleB() => 2;
+
+ void A()
+ {
+ [|SampleA()|];
+ [|SampleB()|];
+ }
+ }
+ """)
+ .ValidateAsync();
+ }
+
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzerTests.cs
deleted file mode 100644
index e3b215754..000000000
--- a/tests/Meziantou.Analyzer.Test/Rules/ValueReturnedByStreamReadShouldBeUsedAnalyzerTests.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using Meziantou.Analyzer.Rules;
-using TestHelper;
-
-namespace Meziantou.Analyzer.Test.Rules;
-
-public sealed class ValueReturnedByStreamReadShouldBeUsedAnalyzerTests
-{
- private static ProjectBuilder CreateProjectBuilder()
- {
- return new ProjectBuilder()
- .WithAnalyzer();
- }
-
- [Fact]
- public async Task Read_ReturnValueNotUsed()
- {
- const string SourceCode = @"using System.IO;
-class Test
-{
- void A()
- {
- var stream = File.OpenRead("""");
- [|stream.Read(null, 0, 0)|];
- }
-}
-";
- await CreateProjectBuilder()
- .WithSourceCode(SourceCode)
- .ValidateAsync();
- }
-
- [Fact]
- public async Task ReadAsync_ReturnValueNotUsed()
- {
- const string SourceCode = @"using System.IO;
-class Test
-{
- async void A()
- {
- var stream = File.OpenRead("""");
- await [|stream.ReadAsync(null, 0, 0)|];
- }
-}
-";
- await CreateProjectBuilder()
- .WithSourceCode(SourceCode)
- .ValidateAsync();
- }
-
- [Fact]
- public async Task ReadAsync_ReturnValueUsed_DiscardOperator()
- {
- const string SourceCode = @"using System.IO;
-class Test
-{
- async void A()
- {
- var stream = File.OpenRead("""");
- _ = await stream.ReadAsync(null, 0, 0);
- }
-}
-";
- await CreateProjectBuilder()
- .WithSourceCode(SourceCode)
- .ValidateAsync();
- }
-
- [Fact]
- public async Task Read_ReturnValueUsed_MethodCall()
- {
- const string SourceCode = @"using System.IO;
-class Test
-{
- async void A()
- {
- var stream = File.OpenRead("""");
- System.Console.Write(stream.Read(null, 0, 0));
- }
-}
-";
- await CreateProjectBuilder()
- .WithSourceCode(SourceCode)
- .ValidateAsync();
- }
-}