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
32 changes: 32 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'tests/**'
- 'benchmarks/**'
- 'tools/**'
- 'Light.GuardClauses.SingleFile.cs'
- '*.slnx'
- '*.props'
- 'global.json'
Expand All @@ -19,6 +20,7 @@ on:
- 'tests/**'
- 'benchmarks/**'
- 'tools/**'
- 'Light.GuardClauses.SingleFile.cs'
- '*.slnx'
- '*.props'
- 'global.json'
Expand Down Expand Up @@ -90,6 +92,36 @@ jobs:
path: artifacts/coverage
if-no-files-found: warn

verify-single-file:
name: Verify generated single file

runs-on: ubuntu-latest
needs: build-and-test

permissions:
contents: read

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
# The exporter defaults to src/Light.GuardClauses and the committed single file at the repository
# root, and it validates that the regenerated file compiles before this job compares it.
- name: Regenerate single file
run: >
dotnet run
--project tools/source-export/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SourceCodeTransformation.csproj
-c Release
- name: Verify that the single file is up to date
run: |
if ! git diff --quiet -- Light.GuardClauses.SingleFile.cs; then
echo "::error file=Light.GuardClauses.SingleFile.cs::Light.GuardClauses.SingleFile.cs is out of date. Run the source export tool and commit the regenerated file."
git diff -- Light.GuardClauses.SingleFile.cs
exit 1
fi

coverage-comment:
name: Coverage comment
runs-on: ubuntu-latest
Expand Down
17 changes: 9 additions & 8 deletions Light.GuardClauses.SingleFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using JetBrains.Annotations;
using Light.GuardClauses.Exceptions;
using Light.GuardClauses.ExceptionFactory;
Expand All @@ -52,7 +53,7 @@ namespace Light.GuardClauses
/// <summary>
/// The <see cref = "Check"/> class provides access to all assertions of Light.GuardClauses.
/// </summary>
// ReSharper disable once RedundantTypeDeclarationBody -- required for Source Code Transformation
// ReSharper disable once RedundantTypeDeclarationBody -- required for Source Code Transformation
internal static class Check
{
/// <summary>
Expand Down Expand Up @@ -4893,7 +4894,7 @@ public static TimeSpan MustBePositive(this TimeSpan parameter, Func<TimeSpan, Ex
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TimeSpan MustBePositiveOrInfinite(this TimeSpan parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > TimeSpan.Zero || parameter == System.Threading.Timeout.InfiniteTimeSpan))
if (!(parameter > TimeSpan.Zero || parameter == Timeout.InfiniteTimeSpan))
{
Throw.MustBePositiveOrInfinite(parameter, parameterName, message);
}
Expand All @@ -4917,7 +4918,7 @@ public static TimeSpan MustBePositiveOrInfinite(this TimeSpan parameter, [Caller
[ContractAnnotation("exceptionFactory:null => halt")]
public static TimeSpan MustBePositiveOrInfinite(this TimeSpan parameter, Func<TimeSpan, Exception> exceptionFactory)
{
if (!(parameter > TimeSpan.Zero || parameter == System.Threading.Timeout.InfiniteTimeSpan))
if (!(parameter > TimeSpan.Zero || parameter == Timeout.InfiniteTimeSpan))
{
Throw.CustomException(exceptionFactory, parameter);
}
Expand Down Expand Up @@ -11871,7 +11872,7 @@ internal static class RegularExpressions
/// <summary>
/// Gets the string that represents the <see cref = "EmailRegex"/>.
/// </summary>
// This is an AI-generated regex. I don't have any clue and I find it way to complex to ever understand it.
// This is an AI-generated regex. I don't have any clue and I find it way to complex to ever understand it.
public const string EmailRegexText = @"^(?:(?:""(?:(?:[^""\\]|\\.)*)""|[\p{L}\p{N}!#$%&'*+\-/=?^_`{|}~-]+(?:\.[\p{L}\p{N}!#$%&'*+\-/=?^_`{|}~-]+)*)@(?:(?:[A-Za-z0-9](?:[A-Za-z0-9\-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}|(?:\[(?:IPv6:[0-9A-Fa-f:.]+)\])|(?:25[0-5]|2[0-4]\d|[01]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d?\d)){3}))$";
/// <summary>
/// Gets the default regular expression for email validation.
Expand Down Expand Up @@ -12628,7 +12629,7 @@ namespace Light.GuardClauses.ExceptionFactory
/// <summary>
/// Provides static factory methods that throw default exceptions.
/// </summary>
// ReSharper disable once RedundantTypeDeclarationBody - requried for the Source Code Transformation
// ReSharper disable once RedundantTypeDeclarationBody - requried for the Source Code Transformation
internal static class Throw
{
/// <summary>
Expand Down Expand Up @@ -14175,7 +14176,7 @@ public static bool Contains(// ReSharper disable once RedundantNullableFlowAttri
/// <summary>
/// Provides extension methods for the <see cref = "string "/> class.
/// </summary>
// ReSharper disable once RedundantTypeDeclarationBody -- required for Source Code Transformation
// ReSharper disable once RedundantTypeDeclarationBody -- required for Source Code Transformation
internal static partial class StringExtensions
{
}
Expand Down Expand Up @@ -14409,7 +14410,7 @@ public static StringBuilder AppendLineIf(// ReSharper disable once RedundantNull
/// specified <paramref name = "stringBuilder"/>.
/// </summary>
/// <exception cref = "ArgumentNullException">Thrown when any parameter is null.</exception>
// ReSharper disable RedundantNullableFlowAttribute
// ReSharper disable RedundantNullableFlowAttribute
public static StringBuilder AppendExceptionMessages([NotNull][ValidatedNotNull] this StringBuilder stringBuilder, [NotNull][ValidatedNotNull] Exception exception)
// ReSharper restore RedundantNullableFlowAttribute
{
Expand All @@ -14434,7 +14435,7 @@ public static StringBuilder AppendExceptionMessages([NotNull][ValidatedNotNull]
/// a single string.
/// </summary>
/// <exception cref = "ArgumentNullException">Thrown when <paramref name = "exception"/> is null.</exception>
// ReSharper disable once RedundantNullableFlowAttribute
// ReSharper disable once RedundantNullableFlowAttribute
public static string GetAllExceptionMessages([NotNull][ValidatedNotNull] this Exception exception) => new StringBuilder().AppendExceptionMessages(exception).ToString();
/// <summary>
/// Checks if the two strings are equal using ordinal sorting rules as well as ignoring the white space
Expand Down
27 changes: 27 additions & 0 deletions ai-plans/0179-source-export-annotation-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Syntax-Aware Source-Export Annotation Cleanup

## Rationale

The source exporter removes annotation usages with exact text replacement and whole-line classification. These approaches miss attributes in combined lists or with alternative argument syntax, and multiline handling currently depends on formatting performed earlier in the export pipeline.

Replace annotation cleanup with a Roslyn syntax transformation so each configured attribute can be removed without disturbing neighboring attributes or nullable-flow metadata. Provide equivalent cleanup support for `NoEnumeration` so exports that omit bundled JetBrains annotations can remain self-contained and compilable.

## Acceptance Criteria

- [x] Configured annotation removal works for standalone, combined, adjacent, and multiline attribute syntax while preserving unrelated attributes and surrounding comments.
- [x] All existing annotation-removal options use the syntax-aware cleanup path, including caller-argument expressions that use `nameof`.
- [x] `RemoveNoEnumeration` is available as an opt-in source-export setting with a default value of `false`.
- [x] The canonical exporter settings and source-inclusion documentation describe `RemoveNoEnumeration`.
- [x] Removing `ValidatedNotNull` or `NoEnumeration` preserves adjacent nullable-flow annotations such as `NotNull` and `NotNullWhen`.
- [x] Automated tests cover the supported attribute layouts and protect the behavior of every annotation-removal option.
- [x] Generated output configured without bundled JetBrains annotation definitions or their using directive compiles successfully when JetBrains-only usages are configured for removal.

## Technical Details

Perform annotation removal on the merged `CompilationUnitSyntax` already maintained by the exporter rather than serializing and reparsing the source. Derive the set of targeted attribute names from `RemoveContractAnnotations`, `RemoveValidatedNotNull`, `RemoveDoesNotReturn`, `RemoveNotNullWhen`, `RemoveCallerArgumentExpressions`, and the new `RemoveNoEnumeration` option.

Inspect `AttributeListSyntax` nodes and remove only matching `AttributeSyntax` children from mixed lists. Remove the complete attribute list when every child is targeted. Preserve exterior trivia when removing nodes, then normalize whitespace after the syntax transformation so comments and unaffected adjacent lists remain well-formed. Attribute matching should use the rightmost name component and treat the optional `Attribute` suffix equivalently; semantic binding is intentionally unnecessary because excluded attribute definitions may be unresolved during export.

Keep non-syntactic formatting cleanup, such as eliminating unwanted blank lines after XML documentation, separate from annotation removal. `RemoveNoEnumeration` must remain independent from `IncludeJetBrainsAnnotations` and `IncludeJetBrainsAnnotationsUsing` because consumers may exclude the bundled definitions while supplying JetBrains annotations externally.

Include focused cleanup tests for partial and complete list removal, multiple adjacent lists, multiline arguments, preservation of trivia, and `CallerArgumentExpression(nameof(parameter))`. Add an end-to-end generated-file build validation using the configuration from issue #178 plus `RemoveNoEnumeration=true`, and assert that targeted names are absent while `NotNull` and `NotNullWhen` remain.
1 change: 1 addition & 0 deletions docs/source-code-inclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Configuration keys are case-insensitive. Nested command-line keys use `:`, for e
| `ChangePublicTypesToInternalTypes` | `true` | Makes exported public types internal |
| `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 |
| `IncludeVersionComment` | `true` | Adds the generated version header |
| `RemoveOverloadsWithExceptionFactory` | `false` | Globally removes assertion overloads that accept exception factories |
Expand Down
Loading
Loading