Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 0 additions & 128 deletions Light.GuardClauses.SingleFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14658,36 +14658,6 @@ internal sealed class NoEnumerationAttribute : Attribute

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Specifies that <see langword="null"/> is allowed as an input even if the
/// corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class AllowNullAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref = "AllowNullAttribute"/> class.
/// </summary>
public AllowNullAttribute()
{
}
}

/// <summary>
/// Specifies that <see langword="null"/> is disallowed as an input even if the
/// corresponding type allows it.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class DisallowNullAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref = "DisallowNullAttribute"/> class.
/// </summary>
public DisallowNullAttribute()
{
}
}

/// <summary>
/// Specifies that a method that will never return under any circumstance.
/// </summary>
Expand All @@ -14702,76 +14672,6 @@ public DoesNotReturnAttribute()
}
}

/// <summary>
/// Specifies that the method will not return if the associated <see cref = "Boolean"/>
/// parameter is passed the specified value.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>
/// Gets the condition parameter value.
/// Code after the method is considered unreachable by diagnostics if the argument
/// to the associated parameter matches this value.
/// </summary>
public bool ParameterValue { get; }

/// <summary>
/// Initializes a new instance of the <see cref = "DoesNotReturnIfAttribute"/>
/// class with the specified parameter value.
/// </summary>
/// <param name = "parameterValue">
/// The condition parameter value.
/// Code after the method is considered unreachable by diagnostics if the argument
/// to the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue)
{
ParameterValue = parameterValue;
}
}

/// <summary>
/// Specifies that an output may be <see langword="null"/> even if the
/// corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class MaybeNullAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref = "MaybeNullAttribute"/> class.
/// </summary>
public MaybeNullAttribute()
{
}
}

/// <summary>
/// Specifies that when a method returns <see cref = "ReturnValue"/>,
/// the parameter may be <see langword="null"/> even if the corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>
/// Gets the return value condition.
/// If the method returns this value, the associated parameter may be <see langword="null"/>.
/// </summary>
public bool ReturnValue { get; }

/// <summary>
/// Initializes the attribute with the specified return value condition.
/// </summary>
/// <param name = "returnValue">
/// The return value condition.
/// If the method returns this value, the associated parameter may be <see langword="null"/>.
/// </param>
public MaybeNullWhenAttribute(bool returnValue)
{
ReturnValue = returnValue;
}
}

/// <summary>
/// Specifies that an output is not <see langword="null"/> even if the
/// corresponding type allows it.
Expand All @@ -14787,34 +14687,6 @@ public NotNullAttribute()
}
}

/// <summary>
/// Specifies that the output will be non-<see langword="null"/> if the
/// named parameter is non-<see langword="null"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>
/// Gets the associated parameter name.
/// The output will be non-<see langword="null"/> if the argument to the
/// parameter specified is non-<see langword="null"/>.
/// </summary>
public string ParameterName { get; }

/// <summary>
/// Initializes the attribute with the associated parameter name.
/// </summary>
/// <param name = "parameterName">
/// The associated parameter name.
/// The output will be non-<see langword="null"/> if the argument to the
/// parameter specified is non-<see langword="null"/>.
/// </param>
public NotNullIfNotNullAttribute(string parameterName)
{
ParameterName = parameterName;
}
}

/// <summary>
/// Specifies that when a method returns <see cref = "ReturnValue"/>,
/// the parameter will not be <see langword="null"/> even if the corresponding type allows it.
Expand Down
37 changes: 37 additions & 0 deletions ai-plans/0181-bundled-annotation-type-reachability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Reachability-Driven Emission of Bundled Annotation Types

## Rationale

`ReSharperAnnotations.cs` is copied into the export as an all-or-nothing unit, so `IncludeJetBrainsAnnotations` is a per-file switch while every annotation-removal option is a per-attribute switch. Enabling the option to obtain `NoEnumerationAttribute` therefore also emits `ContractAnnotationAttribute` and the JetBrains `NotNullAttribute`, neither of which the generated code references once `RemoveContractAnnotations` is enabled. The same wholesale copying applies to the other bundled support types: six of the nine `System.Diagnostics.CodeAnalysis` polyfill attributes are unreferenced in every configuration, which is roughly ninety lines of dead code in the committed single file.

Decide emission of bundled support types by reachability from the annotation usages that survive cleanup, so `IncludeJetBrainsAnnotations` and the polyfill inclusion options mean "provide the support types this export needs" instead of "provide all of them". This keeps the export as deliberate as its whitelist already suggests without introducing per-attribute configuration options.

## Acceptance Criteria

- [x] Bundled support types are emitted only when the generated file references them, directly or transitively, for every combination of the annotation-removal and inclusion options and for whitelisted exports whose retained assertions no longer use an annotation.
- [x] The JetBrains `NotNullAttribute` is emitted only as a dependency of `ContractAnnotationAttribute`, never because of the `[NotNull]` usages on assertion parameters.
- [x] When no JetBrains annotation type survives, the `JetBrains.Annotations` namespace, its license banner, and the `using JetBrains.Annotations;` directive are all absent from the generated file.
- [x] The `NotNullAttribute` alias directive is emitted exactly when the JetBrains `NotNullAttribute` is part of the export.
- [x] The export behaves as before when `IncludeJetBrainsAnnotations` is `false`, including the configuration that imports the JetBrains namespace from an externally supplied package.
- [x] Exports whose bundled types were trimmed still compile through the generated-file build validation.
- [x] The committed `Light.GuardClauses.SingleFile.cs` is regenerated, and its only change is the removal of the unreferenced code-analysis polyfill attributes; the JetBrains annotation types and all guard-clause code are byte-identical.
- [x] Automated tests cover the trimming decisions for the relevant option combinations, including the configuration reported in issue #181.
- [x] The source-inclusion documentation describes emission of bundled support types as reachability-driven, and documents that importing the JetBrains namespace without bundling its types requires the consuming project to supply them.

## Technical Details

Add an `AnnotationReachabilityTrimmer` that transforms the merged `CompilationUnitSyntax` after `CleanupStep.RemoveAnnotations` and before the final `NormalizeWhitespace`. Running after cleanup is essential and is why this cannot live in `SourceReachabilityAnalyzer`: that analyzer inspects the original source files, where every removable attribute usage is still present.

The trimmer operates on a candidate set of bundled support types rather than on all unreferenced types, because public API types must never be trimmed for lack of references. The candidates are the JetBrains annotation types, `ValidatedNotNullAttribute`, and the emitted `System.Diagnostics.CodeAnalysis` and `System.Runtime.CompilerServices` polyfill types. Two of them reach the merged tree as always-processed source files that whitelist mode exempts from reachability analysis, the other two as literal blocks injected by the merger, so the candidate set has to be identified in the final tree rather than derived from either mechanism.

Resolve references with Roslyn symbol analysis rather than name matching. Create a syntax tree from the final root and a compilation over it, reusing the metadata references that `SourceReachabilityAnalyzer` builds today, then bind every `AttributeSyntax` outside the candidate declarations to its containing type symbol. This resolves the `NotNullAttribute` alias exactly, and the `ContractAnnotationAttribute` to JetBrains `NotNullAttribute` edge falls out of the same closure instead of being special-cased: inside `namespace JetBrains.Annotations`, the namespace member wins over the compilation-unit alias. Note that the root reached at that point in the pipeline does not belong to a syntax tree that reflects it, so the tree must be recreated with the export's parse options.

When an attribute cannot be bound, retain every candidate whose simple name matches. A degraded binding must produce a slightly larger file, never one that fails to compile.

Reachability is established from attribute usages only. Documentation `cref` references are invisible to the trimmer, so a retained type must never document a trimmed one; the four `cref` references among the candidates are self-references that disappear with their own declaration, and `GenerateDocumentationFile` is enabled for consuming projects, where a dangling `cref` would surface as a warning.

Derive the JetBrains using directives from the trimmed set only while `IncludeJetBrainsAnnotations` is `true`. When it is `false`, `IncludeJetBrainsAnnotationsUsing` keeps its current meaning so consumers can supply the annotations externally, as established in [0179](0179-source-export-annotation-cleanup.md). Removing the namespace declaration while leaving its import in place is a compile error, so the two decisions must stay coupled. The JetBrains license banner is leading trivia of the `namespace` token and is removed together with the namespace node.

Cover the JetBrains trimming decisions with focused tests for the default configuration, `RemoveContractAnnotations` alone, both JetBrains removal options together, and `IncludeJetBrainsAnnotations=false`. Add an end-to-end build validation for the configuration from issue #181 — `IncludeJetBrainsAnnotations`, `IncludeJetBrainsAnnotationsUsing`, `RemoveContractAnnotations` and `RemoveValidatedNotNull` enabled, `RemoveNoEnumeration` and `IncludeValidatedNotNullAttribute` disabled — asserting that `NoEnumerationAttribute` survives while the other JetBrains types and `ValidatedNotNullAttribute` do not. Cover polyfill trimming with a `netstandard2.0` export that retains the referenced attributes and drops the unreferenced ones.

Default settings keep all three JetBrains annotation types, so the `verify-single-file` CI job protects that half of the change. The polyfill trimming deliberately changes the committed single file; the removed types are `internal`, so no consumer contract is affected.
16 changes: 14 additions & 2 deletions docs/source-code-inclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Copy [`Light.GuardClauses.SingleFile.cs`](../Light.GuardClauses.SingleFile.cs) i

- changes public Light.GuardClauses types to `internal`, avoiding additions to the consuming assembly's public API;
- enables nullable annotations;
- retains JetBrains contract annotations and the code-analysis, validated-null, and caller-argument-expression support needed by the portable source shape;
- retains the JetBrains contract annotations and only those code-analysis, validated-null, and caller-argument-expression support types that the portable source shape actually references;
- includes the full selected-framework assertion surface; and
- contains one flattened source shape with no conditional-compilation directives.

Expand Down Expand Up @@ -49,7 +49,7 @@ Configuration keys are case-insensitive. Nested command-line keys use `:`, for e
| `BaseNamespace` | `Light.GuardClauses` | Rewrites the base namespace |
| `RemoveContractAnnotations` | `false` | Removes JetBrains contract annotations from members |
| `RemoveNoEnumeration` | `false` | Removes JetBrains `NoEnumeration` annotation usages |
| `IncludeJetBrainsAnnotations` / `IncludeJetBrainsAnnotationsUsing` | `true` | Controls the bundled annotation and using directive |
| `IncludeJetBrainsAnnotations` / `IncludeJetBrainsAnnotationsUsing` | `true` | Controls whether the JetBrains annotation types are bundled and whether their namespace is imported |
| `IncludeVersionComment` | `true` | Adds the generated version header |
| `RemoveOverloadsWithExceptionFactory` | `false` | Globally removes assertion overloads that accept exception factories |
| Nullable/validated-null options | varies; see settings | Include supporting attributes or remove their usages |
Expand All @@ -67,6 +67,18 @@ Use the committed settings file for the exact names and defaults of all nullable

The parser selects the appropriate conditional branches and the merger emits no `#if`, `#else`, `#endif`, or other conditional-compilation directives. The result is therefore for the selected target only, not a multi-target source file.

## Bundled support types

Besides the guard-clause code itself, an export can bundle supporting attribute declarations: the JetBrains annotations, `ValidatedNotNullAttribute`, and the `System.Diagnostics.CodeAnalysis` and `System.Runtime.CompilerServices` polyfills that `netstandard2.0` does not provide.

The inclusion options above decide which of these types the export is *allowed* to bundle. Which of them are actually emitted is then decided by reachability: after the configured annotation usages have been removed, the exporter binds the remaining attribute usages and keeps only the support types they reach, directly or transitively. So `IncludeJetBrainsAnnotations` and the polyfill inclusion options mean "provide the support types this export needs", not "provide all of them". Three consequences are worth knowing:

- The JetBrains `NotNullAttribute` is only emitted as a dependency of `ContractAnnotationAttribute`. The `[NotNull]` usages on assertion parameters resolve to `System.Diagnostics.CodeAnalysis.NotNullAttribute`, so `RemoveContractAnnotations` removes both JetBrains types at once and also drops the `NotNullAttribute` alias directive that disambiguates them.
- When no JetBrains annotation type survives, the `JetBrains.Annotations` namespace, its license banner, and `using JetBrains.Annotations;` are all omitted.
- A whitelisted export drops the support types that its retained assertions do not annotate, even though the exporter processes the bundled declaration files as a whole.

`IncludeJetBrainsAnnotationsUsing` is independent of this trimming while `IncludeJetBrainsAnnotations` is `false`: the generated file then imports `JetBrains.Annotations` without declaring it, and the consuming project has to supply the annotation types itself, for example through the [JetBrains.Annotations](https://www.nuget.org/packages/JetBrains.Annotations) NuGet package. Turn `IncludeJetBrainsAnnotationsUsing` off as well and use `RemoveContractAnnotations` plus `RemoveNoEnumeration` to obtain a file with no JetBrains dependency at all.

## Assertion whitelisting and dependency retention

When `AssertionWhitelist.IsEnabled` is `false`, all entry settings are ignored and the full surface for the selected framework is exported.
Expand Down
Loading
Loading