You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Rationale
Light.GuardClauses.SourceCodeTransformationflattens 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 thenetstandard2.0shape (source parsed withNETSTANDARD2_0defined, every#if NET8_0_OR_GREATERmember dropped), while the non-whitelist full export parses with no symbols and keeps all#ifdirectives.That "multi-target" full export is a fiction for the parts that matter: the committed
Light.GuardClauses.SingleFile.cskeeps 80 tiny attribute-level#if NET8_0_OR_GREATERblocks but contains none of the 35 substantial net8-only public members (theINumber<T>generic-math overloads, theSpan/Memoryoverloads ofIsEmailAddress/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
TargetFrameworkoption (NetStandard2_0by default,Net8_0for 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#ifdirectives, 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.Sourceproject is a pure compile-check harness pinned tonetstandard2.0, so it cannot validate aNet8_0export (the net8-only members do not compile there) and it is redundant with the build validator. It is replaced by a multi-targetedLight.GuardClauses.SourceValidationproject into which the validator injects the generated file, building it against the framework that matchesTargetFramework.The whitelist keeps its fail-loud contract: a
Check.<Name>.csfile with no matchingAssertionWhitelistproperty 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
SourceTargetFrameworkenum with valuesNetStandard2_0andNet8_0is introduced, andSourceFileMergeOptionsgetspublic SourceTargetFramework TargetFramework { get; init; } = SourceTargetFramework.NetStandard2_0;. Configuration binds the value by name.SourceTargetFrameworktoCSharpParseOptions, and bothSourceReachabilityAnalyzerandSourceFileMergeruse it so reachability analysis and emitted output always agree on which branch is active. The standaloneNetStandardParseOptionsfield is removed.#ifdirectives are removed from the output, in both whitelist and non-whitelist mode.TargetFramework == NetStandard2_0, the non-whitelist full export reproduces today's netstandard2.0 API surface with no#ifdirectives.TargetFramework == Net8_0, the flattened export definesNET8_0_OR_GREATER, contains the net8-only members (for exampleMustBeGreaterThanOrApproximately<T>with anINumber<T>constraint and theSpan/Memoryemail overloads), and contains no#ifdirectives.IncludeCallerArgumentExpressionAttribute,IncludeCodeAnalysisNullableAttributes) are derived fromSourceTargetFramework: emitted forNetStandard2_0, suppressed forNet8_0, so a net8 export never redefines framework types.Check.<Name>.csfile with no matchingAssertionWhitelistproperty throws a clear, actionable exception.Light.GuardClauses.Sourceproject is removed (and dropped from the solution) and replaced by a multi-targeted (netstandard2.0;net8.0)Light.GuardClauses.SourceValidationproject that compiles a file supplied through aGeneratedSourceFileMSBuild property rather than a co-located source file.GeneratedFileBuildValidatorbuildsLight.GuardClauses.SourceValidationwith the framework matchingSourceTargetFrameworkand 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.Technical Details
Options and config: add
SourceTargetFrameworkand theTargetFrameworkproperty onSourceFileMergeOptions, defaulting toNetStandard2_0. The committedsettings.jsonsets it explicitly. Whitelist mode continues to be gated byAssertionWhitelist.IsEnabledand 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.NetStandardParseOptionswithCreateParseOptions(SourceTargetFramework)returningCSharpParseOptions(LanguageVersion.CSharp12, preprocessorSymbols: …). MapNet8_0to thenet8.0symbol set (the source only branches onNET8_0_OR_GREATER, so that is the essential symbol; include the usualNET8_0/NETCOREAPPfor robustness) andNetStandard2_0to["NETSTANDARD", "NETSTANDARD2_0"]. The metadata references already come from the runningnet8runtime, soINumber<T>and the span/memory overloads resolve for theNet8_0pass without any reference changes.Merger simplification:
sourceParseOptionsalways comes from the factory, andRemoveConditionalCompilationTriviaalways runs (the previouscsharpParseOptions/AssertionWhitelist.IsEnabledbranching 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_GREATERare handled correctly by branch selection alone: included as real nodes underNet8_0, absent underNetStandard2_0.Polyfill options per framework: the merger emits internal copies of
CallerArgumentExpressionAttributeand theSystem.Diagnostics.CodeAnalysisnullable attributes, which are built into net8; emitting them underNet8_0producesCS0436type conflicts. Derive theIncludeCallerArgumentExpressionAttributeandIncludeCodeAnalysisNullableAttributeseffective values fromTargetFramework(on forNetStandard2_0, off forNet8_0) rather than requiring the user to keepsettings.jsonin sync with the framework.Missing whitelist entry: the existing throw in
EnqueueRootCheckMethodsis retained unchanged; the reflection-based name-to-entry mapping keeps the whitelist and theCheck.*.csfiles in sync by failing the export when they diverge.Build validation rework: delete the
Light.GuardClauses.Sourceproject (and its solution entry) and addLight.GuardClauses.SourceValidationwith<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>,<EnableDefaultCompileItems>false</EnableDefaultCompileItems>,<TreatWarningsAsErrors>true</TreatWarningsAsErrors>, and<Compile Include="$(GeneratedSourceFile)" />.GeneratedFileBuildValidator.Validateno longer searches for a co-located.csproj; it runsdotnet build <SourceValidation>.csproj -f <tfm> -p:GeneratedSourceFile=<absolute target path>, where<tfm>maps fromSourceTargetFramework. Keeping the project in the repo lets it inherit central package management fromDirectory.Packages.props(the versionlessSystem.Collections.Immutablereference); 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.Testsusing real files fromLight.GuardClauses. Cover: aNet8_0export contains theINumber<T>and span/memory members and no#if; aNetStandard2_0export omits them and contains no#if; aNet8_0export omits the polyfill attributes and validates againstnet8.0; aNetStandard2_0export validates againstnetstandard2.0; a build failure is surfaced through the exit code with the file left on disk; the existing whitelist behavior still holds under an explicitNetStandard2_0target; and a synthetic assertion file with no whitelist property throws.