Skip to content

Source Code Transformation for .NET 8 #135

Description

@feO2x

Rationale

Light.GuardClauses.SourceCodeTransformation flattens the library into a single source file, and issue 61 added an opt-in assertion whitelist. Today the flatten/whitelist path is hard-wired to the netstandard2.0 shape (source parsed with NETSTANDARD2_0 defined, every #if NET8_0_OR_GREATER member dropped), while the non-whitelist full export parses with no symbols and keeps all #if directives.

That "multi-target" full export is a fiction for the parts that matter: the committed Light.GuardClauses.SingleFile.cs keeps 80 tiny attribute-level #if NET8_0_OR_GREATER blocks but contains none of the 35 substantial net8-only public members (the INumber<T> generic-math overloads, the Span/Memory overloads of IsEmailAddress/MustBeEmailAddress). They are dropped because the merger rebuilds classes from member nodes and discards the closing-brace trivia those tail members attach to. The single source file has therefore always been a netstandard2.0 floor in practice.

We make that explicit and remove the illusion of multi-targeting. The tool always flattens to exactly one target framework, chosen by a new TargetFramework option (NetStandard2_0 by default, Net8_0 for consumers who want a net8 file). Positioning stays: the single source file is the portable netstandard2.0 floor; net8-specific API lives in the compiled NuGet package. Because the tool no longer preserves #if directives, the trailing-trivia member-drop bug disappears by construction and is out of scope here.

Validation is reworked to match the chosen framework. The existing Light.GuardClauses.Source project is a pure compile-check harness pinned to netstandard2.0, so it cannot validate a Net8_0 export (the net8-only members do not compile there) and it is redundant with the build validator. It is replaced by a multi-targeted Light.GuardClauses.SourceValidation project into which the validator injects the generated file, building it against the framework that matches TargetFramework.

The whitelist keeps its fail-loud contract: a Check.<Name>.cs file with no matching AssertionWhitelist property continues to throw, so the whitelist stays the authoritative catalog of exportable assertions and a newly added assertion cannot slip into a whitelisted export unvetted.

Acceptance Criteria

  • A new SourceTargetFramework enum with values NetStandard2_0 and Net8_0 is introduced, and SourceFileMergeOptions gets public SourceTargetFramework TargetFramework { get; init; } = SourceTargetFramework.NetStandard2_0;. Configuration binds the value by name.
  • A single factory maps SourceTargetFramework to CSharpParseOptions, and both SourceReachabilityAnalyzer and SourceFileMerger use it so reachability analysis and emitted output always agree on which branch is active. The standalone NetStandardParseOptions field is removed.
  • The export always flattens to the selected framework: source is parsed with that framework's symbols and all #if directives are removed from the output, in both whitelist and non-whitelist mode.
  • When TargetFramework == NetStandard2_0, the non-whitelist full export reproduces today's netstandard2.0 API surface with no #if directives.
  • When TargetFramework == Net8_0, the flattened export defines NET8_0_OR_GREATER, contains the net8-only members (for example MustBeGreaterThanOrApproximately<T> with an INumber<T> constraint and the Span/Memory email overloads), and contains no #if directives.
  • Polyfill-emitting options that duplicate types built into net8 (IncludeCallerArgumentExpressionAttribute, IncludeCodeAnalysisNullableAttributes) are derived from SourceTargetFramework: emitted for NetStandard2_0, suppressed for Net8_0, so a net8 export never redefines framework types.
  • The whitelist's fail-loud behavior is preserved: a Check.<Name>.cs file with no matching AssertionWhitelist property throws a clear, actionable exception.
  • The Light.GuardClauses.Source project is removed (and dropped from the solution) and replaced by a multi-targeted (netstandard2.0;net8.0) Light.GuardClauses.SourceValidation project that compiles a file supplied through a GeneratedSourceFile MSBuild property rather than a co-located source file.
  • GeneratedFileBuildValidator builds Light.GuardClauses.SourceValidation with the framework matching SourceTargetFramework and the generated file path passed in; on build failure it prints the captured output, returns a non-zero exit code, and leaves the generated file on disk.
  • Automated tests need to be written.

Technical Details

  • Options and config: add SourceTargetFramework and the TargetFramework property on SourceFileMergeOptions, defaulting to NetStandard2_0. The committed settings.json sets it explicitly. Whitelist mode continues to be gated by AssertionWhitelist.IsEnabled and simply layers member pruning on top of the flatten; no framework validation is needed because the framework is always concrete now.

  • Parse-options factory: replace SourceReachabilityAnalyzer.NetStandardParseOptions with CreateParseOptions(SourceTargetFramework) returning CSharpParseOptions(LanguageVersion.CSharp12, preprocessorSymbols: …). Map Net8_0 to the net8.0 symbol set (the source only branches on NET8_0_OR_GREATER, so that is the essential symbol; include the usual NET8_0 / NETCOREAPP for robustness) and NetStandard2_0 to ["NETSTANDARD", "NETSTANDARD2_0"]. The metadata references already come from the running net8 runtime, so INumber<T> and the span/memory overloads resolve for the Net8_0 pass without any reference changes.

  • Merger simplification: sourceParseOptions always comes from the factory, and RemoveConditionalCompilationTrivia always runs (the previous csharpParseOptions / AssertionWhitelist.IsEnabled branching is removed). Reachability still runs only when the whitelist is enabled, using the same parse options so nodes and analysis stay consistent. Because directives are always stripped, tail members inside #if NET8_0_OR_GREATER are handled correctly by branch selection alone: included as real nodes under Net8_0, absent under NetStandard2_0.

  • Polyfill options per framework: the merger emits internal copies of CallerArgumentExpressionAttribute and the System.Diagnostics.CodeAnalysis nullable attributes, which are built into net8; emitting them under Net8_0 produces CS0436 type conflicts. Derive the IncludeCallerArgumentExpressionAttribute and IncludeCodeAnalysisNullableAttributes effective values from TargetFramework (on for NetStandard2_0, off for Net8_0) rather than requiring the user to keep settings.json in sync with the framework.

  • Missing whitelist entry: the existing throw in EnqueueRootCheckMethods is retained unchanged; the reflection-based name-to-entry mapping keeps the whitelist and the Check.*.cs files in sync by failing the export when they diverge.

  • Build validation rework: delete the Light.GuardClauses.Source project (and its solution entry) and add Light.GuardClauses.SourceValidation with <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>, <EnableDefaultCompileItems>false</EnableDefaultCompileItems>, <TreatWarningsAsErrors>true</TreatWarningsAsErrors>, and <Compile Include="$(GeneratedSourceFile)" />. GeneratedFileBuildValidator.Validate no longer searches for a co-located .csproj; it runs dotnet build <SourceValidation>.csproj -f <tfm> -p:GeneratedSourceFile=<absolute target path>, where <tfm> maps from SourceTargetFramework. Keeping the project in the repo lets it inherit central package management from Directory.Packages.props (the versionless System.Collections.Immutable reference); injecting the file by absolute path makes validation work for target files written anywhere, including the tests' TemporaryDirectory. Preserve the existing captured-output / non-zero-exit / leave-file-on-disk failure behavior.

  • Tests: extend Light.GuardClauses.SourceCodeTransformation.Tests using real files from Light.GuardClauses. Cover: a Net8_0 export contains the INumber<T> and span/memory members and no #if; a NetStandard2_0 export omits them and contains no #if; a Net8_0 export omits the polyfill attributes and validates against net8.0; a NetStandard2_0 export validates against netstandard2.0; a build failure is surfaced through the exit code with the file left on disk; the existing whitelist behavior still holds under an explicit NetStandard2_0 target; and a synthetic assertion file with no whitelist property throws.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions