Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8833170
MA0060: generalize to DoNotIgnoreReturnValue with attribute support a…
meziantou Aug 6, 2026
701d7ab
MA0060: remove TextReader.ReadLine from built-in list
meziantou Aug 6, 2026
7fa14cd
MA0060: remove typed BinaryReader.Read* methods from built-in list, k…
meziantou Aug 6, 2026
1eb1009
MA0060: remove Stream.ReadByte from built-in list
meziantou Aug 6, 2026
8276630
MA0060: remove TextReader.ReadLineAsync from built-in list
meziantou Aug 6, 2026
138426d
MA0060: refactor to use nested AnalyzerContext class pattern
meziantou Aug 6, 2026
a99ffdc
MA0060: split out-parameter check into separate AnalyzeArgument handl…
meziantou Aug 6, 2026
e8479cc
MA0060: add GetReturnTypeAttribute/HasReturnTypeAttribute extension m…
meziantou Aug 6, 2026
4daec9e
MA0060: support [Pure] attribute (System.Diagnostics.Contracts and Je…
meziantou Aug 6, 2026
cb9245b
MA0060: early-exit in AnalyzeInvocation when method returns void
meziantou Aug 6, 2026
9b88a1a
MA0060: add string methods, TryParse pattern, and System.Collections.…
meziantou Aug 6, 2026
9b2fb1f
MA0060: add Windows.Win32.Foundation.HRESULT support
meziantou Aug 6, 2026
b40aa58
MA0060: add tests for all built-in method categories
meziantou Aug 6, 2026
268f27d
MA0060: add tests for arrow-method return value scenarios
meziantou Aug 6, 2026
33a2678
MA0060: support assembly-level DoNotIgnore XML doc ids
meziantou Aug 7, 2026
dc11392
MA0060: use symbol set for assembly-level DoNotIgnore ids
meziantou Aug 7, 2026
97c5958
Tests: use AddMeziantouAttributes in MA0060 tests
meziantou Aug 7, 2026
9f68bb3
wip
meziantou Aug 7, 2026
314d86a
wip
meziantou Aug 7, 2026
b2aae07
wip
meziantou Aug 7, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|ℹ️|✔️|✔️|❌|
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
|[MA0057](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0057.md)|Naming|Class name should end with 'Attribute'|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0058](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0058.md)|Naming|Class name should end with 'Exception'|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[MA0059](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0059.md)|Naming|Class name should end with 'EventArgs'|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
|[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|<span title='Warning'>⚠️</span>|✔️|❌||
|[MA0060](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0060.md)|Design|The return value of the method should be used|<span title='Warning'>⚠️</span>|✔️|❌|<span title='MA0060.enable_tryparse_pattern'>✔️</span>|
|[MA0061](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0061.md)|Design|Method overrides should not change default values|<span title='Warning'>⚠️</span>|✔️|✔️|❌|
|[MA0062](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0062.md)|Design|Non-flags enums should not be marked with "FlagsAttribute"|<span title='Warning'>⚠️</span>|✔️|✔️|<span title='MA0062.allow_all_bits_set_value'>✔️</span>|
|[MA0063](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0063.md)|Performance|Use Where before OrderBy|<span title='Info'>ℹ️</span>|✔️|✔️|❌|
Expand Down
134 changes: 129 additions & 5 deletions docs/Rules/MA0060.md
Original file line number Diff line number Diff line change
@@ -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
<!-- sources -->
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)
<!-- sources -->

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<K,V>` | `Clear`, `Add`, `AddRange`, `SetItem`, `SetItems`, `Remove`, `RemoveRange`, `Contains`, `TryGetKey` |
| `System.Collections.Immutable.IImmutableList<T>` | `Clear`, `Add`, `AddRange`, `Insert`, `InsertRange`, `Remove`, `RemoveAll`, `RemoveRange`, `RemoveAt`, `SetItem`, `Replace`, `IndexOf`, `LastIndexOf` |
| `System.Collections.Immutable.IImmutableQueue<T>` | `Clear`, `Enqueue`, `Dequeue`, `Peek` |
| `System.Collections.Immutable.IImmutableSet<T>` | `Clear`, `Add`, `Remove`, `Contains`, `TryGetValue`, `Intersect`, `Except`, `SymmetricExcept`, `Union`, `SetEquals`, `IsProperSubsetOf`, `IsProperSupersetOf`, `IsSubsetOf`, `IsSupersetOf`, `Overlaps` |
| `System.Collections.Immutable.IImmutableStack<T>` | `Clear`, `Push`, `Pop`, `Peek` |
| `System.Collections.Immutable.ImmutableArray` (static) | `Create`, `CreateRange`, `CreateBuilder`, `ToImmutableArray`, `BinarySearch` |
| `System.Collections.Immutable.ImmutableArray<T>.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
}
}
````
25 changes: 25 additions & 0 deletions src/Meziantou.Analyzer.Annotations/DoNotIgnoreAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma warning disable CS1591
#pragma warning disable IDE0060

namespace Meziantou.Analyzer.Annotations;

/// <summary>
/// Indicates that the return value or the value of an <see langword="out"/> parameter must not be ignored.
/// </summary>
[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;
}

/// <summary>Gets the XML documentation id of a method annotated at assembly level.</summary>
public string? XmlDocumentationId { get; }

/// <summary>Gets or sets an optional message explaining why the value must not be ignored.</summary>
public string? Message { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Version>1.5.0</Version>
<Version>1.6.0</Version>
<Description>Annotations to configure Meziantou.Analyzer</Description>
<PackageTags>Meziantou.Analyzer, analyzers</PackageTags>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
}
};
}
}
33 changes: 33 additions & 0 deletions src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading