diff --git a/Build-Docs.ps1 b/Build-Docs.ps1
index d0840178e..a1be3600d 100644
--- a/Build-Docs.ps1
+++ b/Build-Docs.ps1
@@ -27,7 +27,7 @@ Param(
[switch]$ShowDocs
)
-$docFXToolVersion = '2.78.3'
+$docFXToolVersion = '2.78.4'
$InformationPreference = 'Continue'
$ErrorInformationPreference = 'Stop'
diff --git a/Build-Source.ps1 b/Build-Source.ps1
index fa61b8bc0..7c4841629 100644
--- a/Build-Source.ps1
+++ b/Build-Source.ps1
@@ -31,7 +31,7 @@ try
$buildInfo = Initialize-BuildEnvironment -FullInit:$FullInit
# build the Managed code support
- Write-Information "dotnet build 'src\Ubiquity.NET.Llvm.slnx' -c $Configuration"
+ Write-Information "dotnet build --tl:off 'src\Ubiquity.NET.Llvm.slnx' -c $Configuration"
Invoke-External dotnet build --tl:off 'src\Ubiquity.NET.Llvm.slnx' '-c' $Configuration
}
catch
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 6ee686754..ec1aad25c 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -10,6 +10,7 @@
will ensure the folder exists during restore so that it won't fail.
-->
+
@@ -26,6 +27,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3111672ba..9c3a633f9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,55 +1,57 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ -->
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/OneFlow/ReadMe.md b/OneFlow/ReadMe.md
index 186ade1bc..17961dedf 100644
--- a/OneFlow/ReadMe.md
+++ b/OneFlow/ReadMe.md
@@ -4,5 +4,5 @@ This repository follows the [OneFlow](https://www.endoflineblog.com/oneflow-a-gi
model and work-flow. With one active long term branch 'develop'. The master branch is
present and long term but is not active, it only points to the latest official release
(including preview releases) of the project. This is a convenience to allow getting the
-latests released source quickly. Generally the scripts used here are only for release
-managers and are not required (or even an option) for most contributors.
+latest released source quickly. Generally speaking, the scripts used here are only for
+release managers and are not required (or even an option) for most contributors.
diff --git a/Show-Docs.ps1 b/Show-Docs.ps1
index e48e2bde6..8f6e8906e 100644
--- a/Show-Docs.ps1
+++ b/Show-Docs.ps1
@@ -18,7 +18,7 @@ Param(
[string]$DocsPathToHost
)
-$docFXToolVersion = '2.78.3'
+$docFXToolVersion = '2.78.4'
$InformationPreference = 'Continue'
$ErrorInformationPreference = 'Stop'
diff --git a/docfx/CommandLine/toc.yml b/docfx/CommandLine/toc.yml
index 262c8b726..1963d8b3d 100644
--- a/docfx/CommandLine/toc.yml
+++ b/docfx/CommandLine/toc.yml
@@ -1,4 +1,4 @@
-# TOC (Left nav) for the 'extensions' folder
+# TOC (Left nav) for the 'CommandLine' folder
- name: Overview
href: index.md
- name: Namespaces
diff --git a/docfx/ReadMe.md b/docfx/ReadMe.md
index d9249112c..2a9a14e43 100644
--- a/docfx/ReadMe.md
+++ b/docfx/ReadMe.md
@@ -10,19 +10,19 @@ DocFX is used to generate the documentation for this library. There is confusion
***DOES NOT*** mean that the default+modern template is unusable for hosted static site
scenarios like 'gh-pages' in GitHub. It only means that the TOC support will
***require*** a hosted site to provide the contents needed by the generated TOC client side
-scripting. That's it. Don't fear the built-in templates (Despite the lack of decent docs
-explaining the details [Yeah, this project previously fell into those gaps and even
+scripting. That's it. Don't fear the built-in templates - despite the lack of decent docs
+explaining the details! [Yeah, this project previously fell into those gaps and even
constructed a complete custom template to deal with it... Sigh, what a waste of time...
-:facepalm: ])
+:facepalm: ]
## Changes Over Time
DocFX has obsoleted the `docfxconsole` NuGet package that was used to run docfx for a
project via MSBUILD. Instead it focused on a .NET tool to do it all via the command line.
-Ultimately the docfx.json serves as the "project" file for the different site builds.
-The PowerShell script `Build-Docs.ps1` was updated to use the new tool directly. Using that
-script should have little or no impact on the overall flow. There is a "no-targets" project
-in the solution to enable easier access to the input files but does not itself, generate any
-docs - it's just a placeholder.
+Ultimately the docfx.json serves as the corellary to a "project" file for the different site
+builds. The PowerShell script `Build-Docs.ps1` was updated to use the new tool directly.
+Using that script should have little or no impact on the overall flow. There is a
+"no-targets" project in the solution to enable easier access to the input files but does not
+itself, generate any docs - it's just a placeholder.
## Files used by the docs generation
There are a lot of files used to generate the docs and the concept of a Table of Contents
@@ -77,3 +77,76 @@ Since this is generated it is listed in the [.gitignore](#gitignore) file.
#### Library Content
These folders (named after the `*` portion of the [api-*](#api-*) folder names contains
manually written additional files, articles, samples etc... related to a given library.
+
+## Guid to wrting XML DOC comments
+When dealing with doc comments the XML can get in the way of general readability of the
+source code. There is an inherent tension beween how a particular editor renders the docs
+for a symbol/method (VS calls this "Quick Info") and how it is rendered in the final
+documentation by docfx. This guides general use to simplify things as much as possible.
+
+### Lists
+The largest intrusion of the XML into the source is that of lists. The XML doc comments
+official support is to use the `` tag. However, that is VERY intrusive and doesn't
+easily support sub-lists. Consider:
+
+``` C#
+/// Additional steps might include:
+///
+///
+/// - App specific Validation/semantic analysis
+/// - Binding of results to an app specific type
+/// -
+/// Act on the results as proper for the application
+///
+/// - This might include actions parsed but generally isolating the various stages is an easier to understand/maintain model
+/// - Usually this is just app specific code that uses the bound results to adapt behavior
+///
+///
+///
+///
+```
+
+versus:
+``` C#
+/// Additional steps might include:
+///
+/// 1) App specific Validation/semantic analysis
+/// 2) Binding of results to an app specific type
+/// 3) Act on the results as proper for the application
+/// a. This might include actions parsed but generally isolating the various stages is an easier to understand/maintain model
+/// b. Usually this is just app specific code that uses the bound results to adapt behavior
+///
+```
+
+Which one would ***YOU*** rather encounter in code? Which one is easier to understand when
+reading the source? This repo chooses the latter. (If you favor the former, perhaps you
+should reconsider... :grinning:)
+
+#### How to handle lists
+There is little that can be done to alter the rendering of any editor support, at most an
+editor might allow specification of a CSS file, but that is the lowest priority of doc
+comments. Readability by maintainers of the docs AND the rendering for final docs used by
+consumers of of VASTLY higher importance. Still, the editor rendering ***is*** of value to
+maintainers so should not be forgotten as it can make a "right mess of things" even if they
+render properly in final docs.
+
+##### Guidance
+1) ***DO NOT*** use `` tags to include any lists
+ a) Doing so will break the docfx rendering that allows for markdown lists
+2) Use `' tags to indicate a line break. This is used by the editor rendering to mark
+ the end of a line and start a new one. (Stops auto reflow)
+3) Accept that the in edotr rendering might "trim" the lines it shows, eliminating any
+ indentation.
+ a) Sadly, there is no avoiding this. Addition of any sort of "markup" to control that
+ will interfere with the readability AND the final docs rendering.
+4) Always use a different numbering style for sub lists/items
+ b) This will at least show in the in-editor rendering as distinct sub items so if
+ everything is trimmed it is at least a distinct pattern that is readable.
+5) ***DO NOT*** put lists in any place other than inside a `remarks` region
+ a) Usually, the remarks comments are not even rendered as the most useful part is the
+ API signaure and parameter info. Different editors may allow control of that.
+ i) In VS [2019|2022] for C# it is controlled by
+ `Text Editor > C# > Advanced > Editor Help: "Show remarks in Quick Info."`
+ ii) Turning this off can greatly reduce the noise AND reduce the problems of
+ different rende
+
diff --git a/docfx/SrcGeneration/api/index.md b/docfx/SrcGeneration/api/index.md
new file mode 100644
index 000000000..832948997
--- /dev/null
+++ b/docfx/SrcGeneration/api/index.md
@@ -0,0 +1,6 @@
+# Ubiquity.NET.SrcGeneration
+This library contains support functionality to aid in building a source generator.
+This library is multi-targetting to allow consumption from a Roslyn source generator
+or VSIX project. Other uses should leverage the modern runtimes but those cases
+***MUST*** target only `.NET Standard 2.0`
+
diff --git a/docfx/SrcGeneration/index.md b/docfx/SrcGeneration/index.md
new file mode 100644
index 000000000..832948997
--- /dev/null
+++ b/docfx/SrcGeneration/index.md
@@ -0,0 +1,6 @@
+# Ubiquity.NET.SrcGeneration
+This library contains support functionality to aid in building a source generator.
+This library is multi-targetting to allow consumption from a Roslyn source generator
+or VSIX project. Other uses should leverage the modern runtimes but those cases
+***MUST*** target only `.NET Standard 2.0`
+
diff --git a/docfx/SrcGeneration/toc.yml b/docfx/SrcGeneration/toc.yml
new file mode 100644
index 000000000..c6a728a53
--- /dev/null
+++ b/docfx/SrcGeneration/toc.yml
@@ -0,0 +1,5 @@
+# TOC (Left nav) for SrcGeneration folder
+- name: Overview
+ href: index.md
+- name: Namespaces
+ href: api/toc.yml
diff --git a/docfx/antlr-utils/toc.yml b/docfx/antlr-utils/toc.yml
index 21725efca..d5dad6106 100644
--- a/docfx/antlr-utils/toc.yml
+++ b/docfx/antlr-utils/toc.yml
@@ -1,4 +1,4 @@
-# TOC (Left nav) for LLVM folder
+# TOC (Left nav) for antly-utils folder
- name: Overview
href: index.md
- name: Namespaces
diff --git a/docfx/docfx.json b/docfx/docfx.json
index 90e475014..d0cdf9eb2 100644
--- a/docfx/docfx.json
+++ b/docfx/docfx.json
@@ -1,3 +1,8 @@
+// NOTE: docfx does NOT support multi-targetting
+// It can generate docs for ONLY one version of the library
+// docfx merge was supposed to resolve that, but is broken (https://github.com/dotnet/docfx/issues/2289).
+// Sadly that's been open since 2017 so not likely to ever be resolved.
+// Use of this tool is likely abandoned and an alternative should be sought.
{
"$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
"metadata": [
@@ -8,12 +13,12 @@
// to find and check etc...
{
// Interop helpers library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.InteropHelpers",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
"dest": "interop-helpers/api"
@@ -22,63 +27,83 @@
// NOTE: Sample projects are not generating docs, they are... samples 8^)
{
// ANTLR Utilities library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.ANTLR.Utils",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
"dest": "antlr-utils/api"
},
{
// ANTLR Utilities library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.CommandLine",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
"dest": "CommandLine/api"
},
{
// Extensions library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.Extensions",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
- "dest": "extensions/api"
+ "dest": "extensions/api",
+ "properties": {
+ // use .NET 8.0 for the TFM as it is multi-targeting
+ // Sadly, DocFX can't find the dependent project builds if this is .NET 9.0
+ // and then generates warnings as a result. (DocFX metadata generation is
+ // pretty well borked and needs replacement as there are a LOT of workarounds
+ // in this project let alone all the ones found on-line.)
+ "TargetFramework": "net8.0"
+ }
},
{
// LLVM OO Wrappers library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.Llvm",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
"dest": "llvm/api"
},
{
// Runtime utilities library
- "memberLayout":"separatePages",
- "namespaceLayout":"nested",
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
"src": [
{
"src": "../src/Ubiquity.NET.Runtime.Utils",
- "files": ["**.csproj"]
+ "files": [ "**.csproj" ]
}
],
"dest": "runtime-utils/api",
+ },
+ {
+ // SrcGeneration library
+ "memberLayout": "separatePages",
+ "namespaceLayout": "nested",
+ "src": [
+ {
+ "src": "../src/Ubiquity.NET.SrcGeneration",
+ "files": [ "**.csproj" ]
+ }
+ ],
+ "dest": "SrcGeneration/api",
"properties": {
// use .NET 8.0 for the TFM as it is multi-targeting
// Sadly, DocFX can't find the dependent project builds if this is .NET 9.0
@@ -189,6 +214,18 @@
"**/namespaces/**.md",
"**/*-xref.yml"
]
+ },
+ {
+ // SrcGenerations project additional content, Includes the generated metadata API folder
+ // NOTE: File paths are relative to the location of this file
+ "files": [
+ "SrcGeneration/**.{md,yml}"
+ ],
+ // Exclude the namespace overwrites and XREF maps as they are listed explicitly elsewhere
+ "exclude": [
+ "**/namespaces/**.md",
+ "**/*-xref.yml"
+ ]
}
],
"resource": [
diff --git a/docfx/documentation.msbuildproj b/docfx/documentation.msbuildproj
index cec6be247..b187f8d38 100644
--- a/docfx/documentation.msbuildproj
+++ b/docfx/documentation.msbuildproj
@@ -64,6 +64,14 @@
+
+
+
+
+
+
+
+
diff --git a/docfx/extensions/api/index.md b/docfx/extensions/api/index.md
index a453a5d2d..7e6c50857 100644
--- a/docfx/extensions/api/index.md
+++ b/docfx/extensions/api/index.md
@@ -1,7 +1,7 @@
# About
Ubiquity.NET.Extensions contains general extensions for .NET. This is
-a bit of a "grab bag" of functionality used by but not actually part of
-multiple other Ubiquity.NET projects.
+a bit of a [grab bag](https://www.merriam-webster.com/dictionary/grab%20bag) of
+functionality used by but not actually part of multiple other Ubiquity.NET projects.
## Key support
* Computing a hash code for a ReadOnlySpan of bytes using
@@ -10,14 +10,12 @@ multiple other Ubiquity.NET projects.
- This is useful for implementing the RAII pattern in .NET.
* MustUseReturnValueAttribute that is compatible with the [MustUseRetVal](https://github.com/mykolav/must-use-ret-val-fs)
package.
-* StringNormalizer extensions to support converting line endings of strings
- for interoperability.
-* A custom ValidatedNotNullAttribute to allow compiler to assume a parameter
- value is validated as not null.
+* StringNormalizer extensions to support converting line endings of strings for
+ interoperability.
* Fluent style parameter value validation extensions.
- - These are useful when passing parameters to a function that produces a
- result that is fed to the base constructor. These are also useful in body
- expressions to validate input parameters.
+ - These are useful when passing parameters to a function that produces a result that is
+ fed to the base constructor. These are also useful in body expressions to validate
+ input parameters.
* DictionaryBuilder to enable dictionary initializer style initialization of
`ImmutableDictionary` with significantly reduced overhead.
- This leverages an `ImmutableDictionary.Builder` under the hood to build
diff --git a/docfx/index.md b/docfx/index.md
index 833e5e6ed..1883d63d1 100644
--- a/docfx/index.md
+++ b/docfx/index.md
@@ -10,9 +10,10 @@ including JIT execution. Several useful generalized libraries are also included.
| Library | Description |
|---------|-------------|
-| [Ubiquity.NET.Llvm](llvm/index.md) | This library contains The core of the LLVM projection to .NET |
-| [Ubiquity.NET.Runtime.Utils](runtime-utils/index.md) | This library contains common support for DSL runtime and language implementors |
-| [Ubiquity.NET.Extensions](extensions/index.md) | This library contains general extensions and helpers for many scenarios using .NET |
| [Ubiquity.NET.Antlr.Utils](antlr-utils/index.md) | This library contains extensions and helpers for using ANTLR with .NET |
| [Ubiquity.NET.CommandLine](CommandLine/index.md) | This library contains extensions and helpers for command line parsing via `System.CommandLine` |
+| [Ubiquity.NET.Extensions](extensions/index.md) | This library contains general extensions and helpers for many scenarios using .NET |
+| [Ubiquity.NET.Llvm](llvm/index.md) | This library contains The core of the LLVM projection to .NET |
+| [Ubiquity.NET.Runtime.Utils](runtime-utils/index.md) | This library contains common support for DSL runtime and language implementors |
| [Ubiquity.NET.InteropHelpers](interop-helpers/index.md) | This library contains extensions and helpers for implementing interop support for native libraries |
+| [Ubiquity.NET.SrcGeneration](SrcGeneration/index.md) | This library contains extensions and helpers for implementing source generators |
diff --git a/docfx/interop-helpers/toc.yml b/docfx/interop-helpers/toc.yml
index 21725efca..abeb0d1f0 100644
--- a/docfx/interop-helpers/toc.yml
+++ b/docfx/interop-helpers/toc.yml
@@ -1,4 +1,4 @@
-# TOC (Left nav) for LLVM folder
+# TOC (Left nav) for interop-helpers folder
- name: Overview
href: index.md
- name: Namespaces
diff --git a/docfx/runtime-utils/toc.yml b/docfx/runtime-utils/toc.yml
index 21725efca..d76070c1b 100644
--- a/docfx/runtime-utils/toc.yml
+++ b/docfx/runtime-utils/toc.yml
@@ -1,4 +1,4 @@
-# TOC (Left nav) for LLVM folder
+# TOC (Left nav) for runtime-utils folder
- name: Overview
href: index.md
- name: Namespaces
diff --git a/docfx/toc.yml b/docfx/toc.yml
index 567f16aa0..f555478e9 100644
--- a/docfx/toc.yml
+++ b/docfx/toc.yml
@@ -21,3 +21,5 @@
href: interop-helpers/index.md
- name: CommandLine Parsing
href: CommandLine/index.md
+ - name: Source Generation
+ href: SrcGeneration/index.md
diff --git a/src/Analyzers/RepositoryVerifier.UT/AssemblyInfo.cs b/src/Analyzers/RepositoryVerifier.UT/AssemblyInfo.cs
index 69d9f2f2e..54985d396 100644
--- a/src/Analyzers/RepositoryVerifier.UT/AssemblyInfo.cs
+++ b/src/Analyzers/RepositoryVerifier.UT/AssemblyInfo.cs
@@ -1,6 +1,7 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -21,3 +22,4 @@
[assembly: Guid("8ebe7a8e-6ee2-4762-8d6d-333a4497dc96")]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Interop/InteropTests/AssemblyInfo.cs b/src/Interop/InteropTests/AssemblyInfo.cs
index 38f948ad7..4e002590c 100644
--- a/src/Interop/InteropTests/AssemblyInfo.cs
+++ b/src/Interop/InteropTests/AssemblyInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -22,3 +23,4 @@
[assembly: CLSCompliant( false )]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/ANTLR/KaleidoscopeLexer.cs b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/ANTLR/KaleidoscopeLexer.cs
index 4dd870b2c..3e75bdae8 100644
--- a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/ANTLR/KaleidoscopeLexer.cs
+++ b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/ANTLR/KaleidoscopeLexer.cs
@@ -4,7 +4,7 @@
using Antlr4.Runtime;
using Ubiquity.NET.ANTLR.Utils;
-using Ubiquity.NET.Extensions;
+using Ubiquity.NET.Extensions.FluentValidation;
using Ubiquity.NET.Runtime.Utils;
namespace Kaleidoscope.Grammar.ANTLR
diff --git a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/FunctionDefinitionCollection.cs b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/FunctionDefinitionCollection.cs
index 1003ee67b..3ac5d97b1 100644
--- a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/FunctionDefinitionCollection.cs
+++ b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/FunctionDefinitionCollection.cs
@@ -5,7 +5,7 @@
using Kaleidoscope.Grammar.AST;
-using Ubiquity.NET.Extensions;
+using Ubiquity.NET.Extensions.FluentValidation;
namespace Kaleidoscope.Grammar
{
diff --git a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/PrototypeCollection.cs b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/PrototypeCollection.cs
index e883f3164..278de6db8 100644
--- a/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/PrototypeCollection.cs
+++ b/src/Samples/Kaleidoscope/Kaleidoscope.Grammar/AST/PrototypeCollection.cs
@@ -6,7 +6,7 @@
using Kaleidoscope.Grammar.AST;
-using Ubiquity.NET.Extensions;
+using Ubiquity.NET.Extensions.FluentValidation;
namespace Kaleidoscope.Grammar
{
diff --git a/src/Samples/Kaleidoscope/Kaleidoscope.Tests/AssemblyInfo.cs b/src/Samples/Kaleidoscope/Kaleidoscope.Tests/AssemblyInfo.cs
index 69d9f2f2e..54985d396 100644
--- a/src/Samples/Kaleidoscope/Kaleidoscope.Tests/AssemblyInfo.cs
+++ b/src/Samples/Kaleidoscope/Kaleidoscope.Tests/AssemblyInfo.cs
@@ -1,6 +1,7 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -21,3 +22,4 @@
[assembly: Guid("8ebe7a8e-6ee2-4762-8d6d-333a4497dc96")]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs b/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs
index 52593c663..f3266f005 100644
--- a/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs
+++ b/src/Ubiquity.NET.CommandLine.UT/AssemblyInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -22,3 +23,4 @@
[assembly: CLSCompliant( false )]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Ubiquity.NET.CommandLine/ArgsParsing.cs b/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
index 2cd342c48..af5b31595 100644
--- a/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
+++ b/src/Ubiquity.NET.CommandLine/ArgsParsing.cs
@@ -18,20 +18,14 @@ public static class ArgsParsing
/// Settings for the parse
/// Results of the parse
///
- /// Additional steps might include:
- ///
- ///
- /// - App specific Validation/semantic analysis
- /// - Binding of results to an app specific type
- /// -
- /// Act on the results as proper for the application
- ///
- /// - This might include actions parsed but generally isolating the various stages is an easier to understand/maintain model
- /// - Usually this is just app specific code that uses the bound results to adapt behavior
- ///
- ///
- ///
- ///
+ /// Additional steps might include:
+ ///
+ /// 1) App specific Validation/semantic analysis
+ /// 2) Binding of results to an app specific type
+ /// 3) Act on the results as proper for the application
+ /// a. This might include actions parsed but generally isolating the various stages is an easier to understand/maintain model
+ /// b. Usually this is just app specific code that uses the bound results to adapt behavior
+ ///
///
/// This isolation of stages fosters clean implementation AND allows for variances not considered or accounted for in the
/// parsing library. (For instance mutual exclusion of options etc...) validation is an APP specific thing. There
@@ -39,7 +33,7 @@ public static class ArgsParsing
/// on the app and sometimes the runtime environment. (i.e., Should an app maintain strict adherence to a command line options
/// even when such options/patterns are NOT the norm on that environment/OS?)
/// The system already confuses the help and version "options" as they are conceptually
- /// "commands" in terms of that library. (To be fair, the POSIX description it is based on confuses the point as well) This
+ /// "commands" in terms of that library. To be fair, the POSIX description it is based on confuses the point as well. This
/// ambiguity continues by attaching actions and validation to many symbols. While that might seem like it's a good thing,
/// almost every app needs to customize the behavior. Some apps simply can't do so using the current models. Thus, this
/// implementation simply removes the actions and validation to leave both stages to the calling application as it keeps
@@ -124,29 +118,29 @@ public static bool ReportErrors( this ParseResult parseResult, IDiagnosticReport
/// Exit code for the process (only valid when return is (see remarks)
/// if the app should continue and if not
///
- ///
- /// Since this wraps several common operations, some of which may require exiting the app the return value
+ /// Since this wraps several common operations, some of which may require exiting the app, the return value
/// has the semantics of "App should continue". In the event of parse errors or failures in invocation of
/// the default options the result is a with an set to
/// a proper exit code value. (non-zero for errors and zero for success even though the app should still
- /// exit)
- ///
+ /// exit)
+ ///
/// This wraps the common pattern of parsing a command line, invoking default options, and binding the results of a parse
/// using a standard .NET "try pattern". The invocation of default options may return with
- /// set to 0. this indicates the parse was successful AND that the default option was
+ /// set to 0. This indicates the parse was successful AND that the default option was
/// run successfully and the app should exit.
- /// In short this wraps the following sequence of common operations and exiting on completion of
- /// any operation with errors or successful invocation of default options:
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// The is set to the exit code of the app on failures
- ///
+ ///
+ /// In short, this wraps the following sequence of common operations and exiting on completion of
+ /// any operation with errors or successful invocation of default options:
+ /// 1)
+ /// 2)
+ /// 3)
+ /// 4)
+ ///
+ /// The is set to the exit code for the app on failures. This code indicates the
+ /// parse errors and is the result of invoking which, as of the current release,
+ /// is always 1, though this behavior is not documented and therefore subject to change. Thus, calling applications
+ /// should ***NOT*** rely on this value and instead use their own value to indicate a parse error that is documented
+ /// and stable.
///
public static bool TryParse(
string[] args,
diff --git a/src/Ubiquity.NET.Extensions.UT/AssemblyExtensionsTests.cs b/src/Ubiquity.NET.Extensions.UT/AssemblyExtensionsTests.cs
new file mode 100644
index 000000000..2281d08df
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions.UT/AssemblyExtensionsTests.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Reflection;
+
+namespace Ubiquity.NET.Extensions.UT
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public sealed class AssemblyExtensionsTests
+ {
+ [TestMethod]
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ public void GetInformationalVersion_with_null_throws( )
+ {
+ var ex = Assert.ThrowsExactly( ()=> _ = AssemblyExtensions.GetInformationalVersion( null ) );
+ Assert.AreEqual("null", ex.ParamName, "CallerExpressionArgumentAttribute should provide the expression used");
+
+ ex = Assert.ThrowsExactly( ()=> _ = AssemblyExtensions.GetInformationalVersion( null, "self" ) );
+ Assert.AreEqual( "self", ex.ParamName, "explicit value for CallerExpressionArgumentAttribute should override it" );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void GetInformationalVersion_succeeds( )
+ {
+ var thisAsm = typeof( AssemblyExtensionsTests ).Assembly;
+
+ var assemblyVersionAttribute = thisAsm.GetCustomAttribute();
+
+ string expected = assemblyVersionAttribute is not null
+ ? assemblyVersionAttribute.InformationalVersion
+ : thisAsm.GetName().Version?.ToString() ?? string.Empty;
+
+ string actual = thisAsm.GetInformationalVersion();
+ Assert.AreEqual( expected, actual );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions.UT/AssemblyInfo.cs b/src/Ubiquity.NET.Extensions.UT/AssemblyInfo.cs
index 2f445914a..a4d0b1392 100644
--- a/src/Ubiquity.NET.Extensions.UT/AssemblyInfo.cs
+++ b/src/Ubiquity.NET.Extensions.UT/AssemblyInfo.cs
@@ -1,10 +1,6 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System.Runtime.InteropServices;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
// In SDK-style projects such as this one, several assembly attributes that were historically
// defined in this file are now automatically added during build and populated with
// values defined in project properties. For details of which attributes are included
@@ -25,7 +21,12 @@
// see: https://github.com/microsoft/testfx/issues/5555#issuecomment-3448956323
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+// can't use this at assembly level as it isn't supported there for downlevel... [Sigh...]
+//[assembly: ExcludeFromCodeCoverage]
+
// NOTE: use of this and `internal` test classes results in a flurry of
-// error CA1812: '' is an internal class that is apparently never instantiated. If so, remove the code from the assembly. If this class is intended to contain only static members, make it 'static' (Module in Visual Basic). (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812)
+// error CA1812: '' is an internal class that is apparently never instantiated. If so, remove the code from the assembly.
+// If this class is intended to contain only static members, make it 'static' (Module in Visual Basic).
+// (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812)
// In other words, not worth the bother...
// [assembly: DiscoverInternals]
diff --git a/src/Ubiquity.NET.Extensions.UT/DictionaryBuilderTests.cs b/src/Ubiquity.NET.Extensions.UT/DictionaryBuilderTests.cs
index 3cd646324..730e9f8e5 100644
--- a/src/Ubiquity.NET.Extensions.UT/DictionaryBuilderTests.cs
+++ b/src/Ubiquity.NET.Extensions.UT/DictionaryBuilderTests.cs
@@ -1,14 +1,10 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System;
-using System.Collections.Immutable;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
namespace Ubiquity.NET.Extensions.UT
{
[TestClass]
+ [ExcludeFromCodeCoverage]
public sealed class DictionaryBuilderTests
{
[TestMethod]
diff --git a/src/Ubiquity.NET.Extensions.UT/DisposableActionTests.cs b/src/Ubiquity.NET.Extensions.UT/DisposableActionTests.cs
index ed3450586..b993dae93 100644
--- a/src/Ubiquity.NET.Extensions.UT/DisposableActionTests.cs
+++ b/src/Ubiquity.NET.Extensions.UT/DisposableActionTests.cs
@@ -1,13 +1,10 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
namespace Ubiquity.NET.Extensions.UT
{
[TestClass]
+ [ExcludeFromCodeCoverage]
public sealed class DisposableActionTests
{
[TestMethod]
diff --git a/src/Ubiquity.NET.Extensions.UT/FluentValidationExtensionsTests.UT.cs b/src/Ubiquity.NET.Extensions.UT/FluentValidation/ExceptionValidationExtensionsTests.cs
similarity index 77%
rename from src/Ubiquity.NET.Extensions.UT/FluentValidationExtensionsTests.UT.cs
rename to src/Ubiquity.NET.Extensions.UT/FluentValidation/ExceptionValidationExtensionsTests.cs
index 70c986c11..ff0021d38 100644
--- a/src/Ubiquity.NET.Extensions.UT/FluentValidationExtensionsTests.UT.cs
+++ b/src/Ubiquity.NET.Extensions.UT/FluentValidation/ExceptionValidationExtensionsTests.cs
@@ -1,22 +1,20 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System;
-using System.ComponentModel;
+using Ubiquity.NET.Extensions.FluentValidation;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-namespace Ubiquity.NET.Extensions.UT
+namespace Ubiquity.NET.Extensions.UT.FluentValidation
{
[TestClass]
- public sealed class FluentValidationExtensionsTests
+ [ExcludeFromCodeCoverage]
+ public sealed class ExceptionValidationExtensionsTests
{
[TestMethod]
public void ThrowIfNull_throws_expected_exception_when_null( )
{
var ex = Assert.ThrowsExactly(()=>
{
- FluentValidationExtensions.ThrowIfNull( null );
+ ExceptionValidationExtensions.ThrowIfNull( null );
} );
Assert.AreEqual("null", ex.ParamName, "parameter name should match input expression");
}
@@ -26,7 +24,7 @@ public void ThrowIfNull_does_not_throw_on_non_null_input()
{
const string input = "This is a test";
- Assert.AreSame(input, FluentValidationExtensions.ThrowIfNull(input), "Fluent API should return input value on success" );
+ Assert.AreSame(input, ExceptionValidationExtensions.ThrowIfNull(input), "Fluent API should return input value on success" );
}
[TestMethod]
@@ -35,7 +33,7 @@ public void ThrowIfNull_reports_exception_whith_provided_expression( )
const string exp = "My-Expression";
var ex = Assert.ThrowsExactly(()=>
{
- FluentValidationExtensions.ThrowIfNull( null, exp );
+ ExceptionValidationExtensions.ThrowIfNull( null, exp );
} );
Assert.AreEqual( exp, ex.ParamName, "parameter name should match input expression" );
}
@@ -43,7 +41,7 @@ public void ThrowIfNull_reports_exception_whith_provided_expression( )
[TestMethod]
public void ThrowIfNotDefined_does_not_throw_for_defined_value()
{
- Assert.AreEqual(TestEnum.Max, FluentValidationExtensions.ThrowIfNotDefined(TestEnum.Max), "Fluent API should return input value on success" );
+ Assert.AreEqual(TestEnum.Max, ExceptionValidationExtensions.ThrowIfNotDefined(TestEnum.Max), "Fluent API should return input value on success" );
}
[TestMethod]
@@ -53,7 +51,7 @@ public void ThrowIfOutOfRange_does_not_throw_for_inrange_values( )
double min = 0.0;
double max = 2.0;
- Assert.AreEqual(value, FluentValidationExtensions.ThrowIfOutOfRange(value, min, max), "Fluent API should return input value on success");
+ Assert.AreEqual(value, ExceptionValidationExtensions.ThrowIfOutOfRange(value, min, max), "Fluent API should return input value on success");
}
[TestMethod]
@@ -65,7 +63,7 @@ public void ThrowIfOutOfRange_throws_for_out_of_range_values( )
var ex = Assert.ThrowsExactly(()=>
{
- _ = FluentValidationExtensions.ThrowIfOutOfRange( value, min, max );
+ _ = ExceptionValidationExtensions.ThrowIfOutOfRange( value, min, max );
} );
Assert.AreEqual(value, ex.ActualValue);
Assert.AreEqual(nameof(value), ex.ParamName);
@@ -81,7 +79,7 @@ public void ThrowIfOutOfRange_throws_with_custom_expression_for_out_of_range_val
const string exp = "My Expression";
var ex = Assert.ThrowsExactly(()=>
{
- _ = FluentValidationExtensions.ThrowIfOutOfRange( value, min, max, exp );
+ _ = ExceptionValidationExtensions.ThrowIfOutOfRange( value, min, max, exp );
} );
Assert.AreEqual( value, ex.ActualValue );
Assert.AreEqual( exp, ex.ParamName );
@@ -93,14 +91,14 @@ public void ThrowIfNotDefined_throws_for_undefined_values( )
var temp = (TestEnum)4;
var ex = Assert.ThrowsExactly( ( ) =>
{
- FluentValidationExtensions.ThrowIfNotDefined(temp);
+ ExceptionValidationExtensions.ThrowIfNotDefined(temp);
} );
Assert.AreEqual(nameof(temp), ex.ParamName, "parameter name should match input expression" );
var temp2 = (TestByteEnum)4;
var ex2 = Assert.ThrowsExactly( ( ) =>
{
- FluentValidationExtensions.ThrowIfNotDefined(temp2);
+ ExceptionValidationExtensions.ThrowIfNotDefined(temp2);
} );
Assert.AreEqual( nameof( temp2 ), ex2.ParamName, "parameter name should match input expression" );
@@ -108,7 +106,7 @@ public void ThrowIfNotDefined_throws_for_undefined_values( )
var temp3 = (TestU64Enum)int.MaxValue;
var ex3 = Assert.ThrowsExactly( ( ) =>
{
- FluentValidationExtensions.ThrowIfNotDefined(temp3);
+ ExceptionValidationExtensions.ThrowIfNotDefined(temp3);
} );
Assert.AreEqual( nameof( temp3 ), ex3.ParamName, "parameter name should match input expression" );
@@ -117,7 +115,7 @@ public void ThrowIfNotDefined_throws_for_undefined_values( )
var temp4 = (TestU64Enum)(UInt64.MaxValue - 1);
var ex4 = Assert.ThrowsExactly( ( ) =>
{
- FluentValidationExtensions.ThrowIfNotDefined(temp4);
+ ExceptionValidationExtensions.ThrowIfNotDefined(temp4);
} );
Assert.IsNull( ex4.ParamName, "parameter name not available for non-int formattable enums" );
}
diff --git a/src/Ubiquity.NET.Extensions.UT/GlobalNamespaceImports.cs b/src/Ubiquity.NET.Extensions.UT/GlobalNamespaceImports.cs
new file mode 100644
index 000000000..d21b98d8c
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions.UT/GlobalNamespaceImports.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+/*
+NOTE:
+While the MsBuild `ImplicitUsings` property is banned from this repo, the C# language feature of global usings is NOT.
+The build property will auto include an invisible and undiscoverable (without looking up obscure documentation)
+set of namespaces that is NOT consistent or controlled by the developer. THAT is what is BAD/BROKEN about that feature.
+By banning it's use and then providing a `GlobalNamespaceImports.cs` source file with ONLY global using statements ALL of
+that is eliminated. Such use of the language feature restores FULL control and visibility of the namespaces to the developer,
+where it belongs. For a good explanation of this problem see: https://rehansaeed.com/the-problem-with-csharp-10-implicit-usings/.
+For an explanation of the benefits of the language feature see: https://www.hanselman.com/blog/implicit-usings-in-net-6
+*/
+
+// BUG: False positive from IDE0005 - Using directive is unnecessary
+// Attempts to remove/sort are at least able to figure it out and do the right thing.
+// Bug seems to be related to multi-targetting.
+#pragma warning disable IDE0005
+
+global using System;
+global using System.Collections.Generic;
+global using System.Collections.Immutable;
+global using System.ComponentModel;
+global using System.Diagnostics.CodeAnalysis;
+global using System.Runtime.InteropServices;
+
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
diff --git a/src/Ubiquity.NET.Extensions.UT/GlobalSuppressions.cs b/src/Ubiquity.NET.Extensions.UT/GlobalSuppressions.cs
index 145cd6fdf..caa5d65f9 100644
--- a/src/Ubiquity.NET.Extensions.UT/GlobalSuppressions.cs
+++ b/src/Ubiquity.NET.Extensions.UT/GlobalSuppressions.cs
@@ -6,6 +6,4 @@
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
-using System.Diagnostics.CodeAnalysis;
-
[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test module" )]
diff --git a/src/Ubiquity.NET.Extensions.UT/KvpArrayBuilderTests.cs b/src/Ubiquity.NET.Extensions.UT/KvpArrayBuilderTests.cs
index 2dfc5bac4..ebbf1a5d9 100644
--- a/src/Ubiquity.NET.Extensions.UT/KvpArrayBuilderTests.cs
+++ b/src/Ubiquity.NET.Extensions.UT/KvpArrayBuilderTests.cs
@@ -1,15 +1,10 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
namespace Ubiquity.NET.Extensions.UT
{
[TestClass]
+ [ExcludeFromCodeCoverage]
public sealed class KvpArrayBuilderTests
{
[TestMethod]
diff --git a/src/Ubiquity.NET.Extensions.UT/PolyFillExceptionValidatorsTests.cs b/src/Ubiquity.NET.Extensions.UT/PolyFillExceptionValidatorsTests.cs
new file mode 100644
index 000000000..fc3dda87f
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions.UT/PolyFillExceptionValidatorsTests.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+// .NET 7 added the various exception static methods for parameter validation
+// This will back fill them for earlier versions.
+//
+// NOTE: C #14 extension keyword support is required to make this work.
+#if !NET7_0_OR_GREATER
+
+namespace Ubiquity.NET.Extensions.UT
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public sealed class PolyFillExceptionValidatorsTests
+ {
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ [TestMethod]
+ public void ThrowIfNullOrWhiteSpace_throws_if_null_or_whitespace( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( null ));
+ Assert.AreEqual( "null", ex.ParamName, "Compiler should provide expression as name" );
+
+ ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( null, "self" ) );
+ Assert.AreEqual( "self", ex.ParamName, "explicit name should override compiler" );
+
+ var argEx = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace(" \t "));
+ Assert.AreEqual( "\" \\t \"", argEx.ParamName, "Compiler should provide expression as name" );
+
+ argEx = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( " \t ", "self" ) );
+ Assert.AreEqual( "self", argEx.ParamName, "explicit name should override compiler" );
+
+ argEx = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( string.Empty ) );
+ Assert.AreEqual( "string.Empty", argEx.ParamName, "Compiler should provide expression as name" );
+
+ argEx = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNullOrWhiteSpace( string.Empty, "self" ) );
+ Assert.AreEqual( "self", argEx.ParamName, "explicit name should override compiler" );
+ }
+
+ [TestMethod]
+ public void ThrowIfNull_throws_if_null( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNull( null ));
+ Assert.AreEqual( "null", ex.ParamName, "Compiler should provide expression as name" );
+
+ ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNull( null, "self" ) );
+ Assert.AreEqual( "self", ex.ParamName, "explicit name should override compiler" );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void ThrowIf_throws_as_expected( )
+ {
+ object instance = new();
+
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIf(true, instance));
+ Assert.AreEqual( "System.Object", ex.ObjectName );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIf( false, instance );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfEqual_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfEqual(1, 1));
+ Assert.AreEqual( "1", ex.ParamName );
+ Assert.AreEqual( 1, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfEqual( 1, 0 );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfNotEqual_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfNotEqual(1, 0));
+ Assert.AreEqual( "1", ex.ParamName );
+ Assert.AreEqual( 1, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfNotEqual( 1, 1 );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfGreaterThan_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfGreaterThan(1, 0));
+ Assert.AreEqual( "1", ex.ParamName );
+ Assert.AreEqual( 1, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfGreaterThan( 0, 1 );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfGreaterThanOrEqual_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfGreaterThanOrEqual(1, 1));
+ Assert.AreEqual( "1", ex.ParamName );
+ Assert.AreEqual( 1, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfGreaterThanOrEqual( 0, 1 );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfLessThan_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfLessThan(0, 1));
+ Assert.AreEqual( "0", ex.ParamName );
+ Assert.AreEqual( 0, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfLessThan( 1, 0 );
+ }
+
+ [TestMethod]
+ public void ArgumentOutOfRangeExcpetion_ThrowIfLessThanOrEqual_operates_as_expected( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillExceptionValidators.ThrowIfLessThanOrEqual(1, 1));
+ Assert.AreEqual( "1", ex.ParamName );
+ Assert.AreEqual( 1, ex.ActualValue );
+
+ // should not throw
+ PolyFillExceptionValidators.ThrowIfLessThanOrEqual( 1, 0 );
+ }
+ }
+}
+#endif
diff --git a/src/Ubiquity.NET.Extensions.UT/PolyFillOperatingSystemTests.cs b/src/Ubiquity.NET.Extensions.UT/PolyFillOperatingSystemTests.cs
new file mode 100644
index 000000000..7092894f4
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions.UT/PolyFillOperatingSystemTests.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.Extensions.UT
+{
+ [TestClass]
+ public sealed class PolyFillOperatingSystemTests
+ {
+ [TestMethod]
+ public void IsWindows_reports_correct_value( )
+ {
+ bool isWindows = Environment.OSVersion.Platform switch
+ {
+ PlatformID.Win32S or
+ PlatformID.Win32Windows or
+ PlatformID.Win32NT or
+ PlatformID.WinCE => true,
+ _ => false,
+ };
+
+ Assert.AreEqual(isWindows, OperatingSystem.IsWindows());
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions.UT/PolyFillStringExtensionsTests.cs b/src/Ubiquity.NET.Extensions.UT/PolyFillStringExtensionsTests.cs
new file mode 100644
index 000000000..9d9d4e56a
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions.UT/PolyFillStringExtensionsTests.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+#if !NET6_0_OR_GREATER
+// need to import the namespace to allow implicit access to extensions
+using System.Text;
+#endif
+
+namespace Ubiquity.NET.Extensions.UT
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public sealed class PolyFillStringExtensionsTests
+ {
+ // since these are ONLY an extension with runtime's prior to .NET 6
+ // test the handling. For .NET6 or later, that's up to .NET itself to test
+#if !NET6_0_OR_GREATER
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ [TestMethod]
+ public void Methods_throw_on_invalid_input( )
+ {
+ var ex = Assert.ThrowsExactly( ( ) => PolyFillStringExtensions.ReplaceLineEndings( null ));
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) => PolyFillStringExtensions.ReplaceLineEndings( null, "replacement" ));
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) => PolyFillStringExtensions.ReplaceLineEndings( "source text", null ) );
+ Assert.AreEqual( "replacementText", ex.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+#endif
+
+ // Technically this tests both poly filled and official implementations, but validates the
+ // assumptions that exist betweeen them. If both pass then the poly fill is replicating the
+ // tested behavior of the official runtime implementation. (If, perhaps, less performant...)
+
+ [TestMethod]
+ public void ReplaceLineEndings_uses_Environment_newlines( )
+ {
+ const string inputMixedLines = "line0\r\nline1\rline2\nline3\fline4\u0085line5\u2028line6\u2029";
+ string expected = "line0" + Environment.NewLine
+ + "line1" + Environment.NewLine
+ + "line2" + Environment.NewLine
+ + "line3" + Environment.NewLine
+ + "line4" + Environment.NewLine
+ + "line5" + Environment.NewLine
+ + "line6" + Environment.NewLine;
+
+ string actual = inputMixedLines.ReplaceLineEndings();
+ Assert.AreEqual(expected, actual);
+ }
+
+ [TestMethod]
+ public void ReplaceLineEndings_uses_provided_string( )
+ {
+ const string inputMixedLines = "line0\r\nline1\rline2\nline3\fline4\u0085line5\u2028line6\u2029";
+ const string expected = "line0line1line2line3line4line5line6";
+
+ string actual = inputMixedLines.ReplaceLineEndings("");
+ Assert.AreEqual( expected, actual );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions.UT/StringNormalizerTests.cs b/src/Ubiquity.NET.Extensions.UT/StringNormalizerTests.cs
index a72f56f42..e310727fd 100644
--- a/src/Ubiquity.NET.Extensions.UT/StringNormalizerTests.cs
+++ b/src/Ubiquity.NET.Extensions.UT/StringNormalizerTests.cs
@@ -1,13 +1,11 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using System;
-
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
namespace Ubiquity.NET.Extensions.UT
{
- // NOTE: Test output will include special characters for CR and LF characters that makes it easy to visualize
+ // NOTE: Test output will include special characters for CR (U+240D) and LF (U+240A) characters that makes it easy to visualize
+ // line ending differences.
+ //
// Example:
// Assert.AreEqual failed. Expected string length 51 but was 52. 'expected' expression: 'expectedOutput', 'actual' expression: 'systemNormalizedInput'.
// Expected: "This is a line␊This is anotherline␊And..."
@@ -16,15 +14,17 @@ namespace Ubiquity.NET.Extensions.UT
// NOTE: In C# the string "line1\nLine2" has exactly what was put in the string
// That is, it contains a SINGLE LF '\n' character. NOT an environment
- // specific "newline". Thus, if a string needs to represent an platform specific
+ // specific "newline". Thus, if a string needs to represent a platform specific
// newline sequence it can use `Environment.NewLine` or `string.ReplaceLineEndings()`.
// The docs on `ReplaceLineEndings()` are silent on the point of input forms
- // replaced. However, seplunking the code indicates it follows Unicode standard §5.8,
+ // replaced. However, spelunking the code indicates it follows Unicode standard §5.8,
// Recommendation R4 and Table 5-2 (CR, LF, CRLF, NEL, LS, FF, PS). Explicitly excluded
// is VT. Thus, that will normalize ANY newline sequence to the form expected by the
// environment.
+ // Unfortunately, ReplaceLineEndings() is NOT available in downlevel runtimes...
[TestClass]
+ [ExcludeFromCodeCoverage]
public sealed class StringNormalizerTests
{
[TestMethod]
@@ -36,7 +36,7 @@ public void System_line_ending_detected_correctly( )
[TestMethod]
public void Normalize_with_default_endings_does_nothing( )
{
- string testInput = "This is a line\nAnd so is this".ReplaceLineEndings(); // Platform sepecific
+ string testInput = "This is a line" + Environment.NewLine + "And so is this"; // Platform sepecific
string normalizedOutput = testInput.NormalizeLineEndings(StringNormalizer.SystemLineEndings);
Assert.AreSame(testInput, normalizedOutput, "Should return same instance (zero copy)");
}
@@ -44,7 +44,7 @@ public void Normalize_with_default_endings_does_nothing( )
[TestMethod]
public void Normalize_with_alternate_endings_produces_new_string( )
{
- string testInput = "This is a line\nAnd so is this".ReplaceLineEndings(); // Platform sepecific
+ string testInput = "This is a line"+ Environment.NewLine + "And so is this"; // Platform sepecific
const string expectedOutput = "This is a line\rAnd so is this";
// CR Only is not the default for any currently supported runtinme for .NET so this
@@ -60,7 +60,7 @@ public void Normalize_with_alternate_endings_produces_new_string( )
public void Normalize_with_mixed_input_succeeds()
{
const string mixedInput = "This is a line\r\nThis is anotherline\rAnd aonther line";
- string expectedOutput = "This is a line\nThis is anotherline\nAnd aonther line".ReplaceLineEndings(); // Platform sepecific
+ string expectedOutput = "This is a line"+ Environment.NewLine + "This is anotherline"+ Environment.NewLine + "And aonther line"; // Platform sepecific
string systemNormalizedInput = mixedInput.NormalizeLineEndings(LineEndingKind.MixedOrUnknownEndings, StringNormalizer.SystemLineEndings);
Assert.AreEqual(expectedOutput, systemNormalizedInput);
}
diff --git a/src/Ubiquity.NET.Extensions.UT/Ubiquity.NET.Extensions.UT.csproj b/src/Ubiquity.NET.Extensions.UT/Ubiquity.NET.Extensions.UT.csproj
index b93ceba3e..4013ddc85 100644
--- a/src/Ubiquity.NET.Extensions.UT/Ubiquity.NET.Extensions.UT.csproj
+++ b/src/Ubiquity.NET.Extensions.UT/Ubiquity.NET.Extensions.UT.csproj
@@ -1,18 +1,20 @@
-
- net8.0
- false
- True
-
+
+ net8.0;net481
+
+ preview
+ false
+ True
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
diff --git a/src/Ubiquity.NET.Extensions/AssemblyExtensions.cs b/src/Ubiquity.NET.Extensions/AssemblyExtensions.cs
index fde6e1e3a..a6ba44c0f 100644
--- a/src/Ubiquity.NET.Extensions/AssemblyExtensions.cs
+++ b/src/Ubiquity.NET.Extensions/AssemblyExtensions.cs
@@ -3,26 +3,42 @@
namespace Ubiquity.NET.Extensions
{
- // This does NOT use the new C# 14 extension syntax due to several reasons
- // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end]
- // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx])
- // 3) Many tools (like docfx don't support the new syntax yet)
- // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]).
- //
- // Bottom line it's a good idea with an incomplete implementation lacking support
- // in the overall ecosystem. Don't use it unless you absolutely have to until all
- // of that is sorted out.
-
/// Utility class to provide extensions for consumers
+ [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "Extension" )]
public static class AssemblyExtensions
{
- /// Gets the value of the from an assembly
- /// Assembly to get informational version from
- /// Expression for ; Normally set by compiler.
- /// Information version of the assembly or an empty string if not available
- public static string GetInformationalVersion(this Assembly self, [CallerArgumentExpression(nameof(self))] string? exp = null)
+ // VS2026 builds of this are OK, however command line/PR/CI builds will generate an error
+ // No idea why there's a difference the $(NETCoreSdkVersion) is the same in both so it's
+ // unclear why the two things behave differently. This is just another example of why the
+ // `extension` keyword is "not yet ready for prime time". Too many things don't support it
+ // properly yet. [Hopefully, that works itself out in short order as it's useless unless
+ // fully supported]
+#if COMPILER_SUPPORTS_CALLER_ATTRIBUES_ON_EXTENSION
+ extension(Assembly self)
+ {
+ /// Gets the informational version from an assembly
+ /// Expresssion for the assembly to retrieve the attribute data from; normally provided by compiler
+ /// String contents from the in the assembly or
+ [SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "Instance extension" )]
+ public string GetInformationalVersion( [CallerArgumentExpression( nameof( self ) )] string? exp = null )
+ {
+ ArgumentNullException.ThrowIfNull( self, exp );
+
+ var assemblyVersionAttribute = self.GetCustomAttribute();
+
+ return assemblyVersionAttribute is not null
+ ? assemblyVersionAttribute.InformationalVersion
+ : self.GetName().Version?.ToString() ?? string.Empty;
+ }
+ }
+#else
+ /// Gets the informational version from an assembly
+ /// Assembly to extract the version from
+ /// Expresssion for the assembly to retrieve the attribute data from; normally provided by compiler
+ /// String contents from the in the assembly or
+ public static string GetInformationalVersion(this Assembly self, [CallerArgumentExpression( nameof( self ) )] string? exp = null )
{
- ArgumentNullException.ThrowIfNull(self, exp);
+ ArgumentNullException.ThrowIfNull( self, exp );
var assemblyVersionAttribute = self.GetCustomAttribute();
@@ -30,5 +46,7 @@ public static string GetInformationalVersion(this Assembly self, [CallerArgument
? assemblyVersionAttribute.InformationalVersion
: self.GetName().Version?.ToString() ?? string.Empty;
}
+#endif
+
}
}
diff --git a/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs b/src/Ubiquity.NET.Extensions/FluentValidation/ExceptionValidationExtensions.cs
similarity index 82%
rename from src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs
rename to src/Ubiquity.NET.Extensions/FluentValidation/ExceptionValidationExtensions.cs
index 8352cd8b1..21c6bb383 100644
--- a/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs
+++ b/src/Ubiquity.NET.Extensions/FluentValidation/ExceptionValidationExtensions.cs
@@ -1,12 +1,12 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-namespace Ubiquity.NET.Extensions
+namespace Ubiquity.NET.Extensions.FluentValidation
{
// This does NOT use the new C# 14 extension syntax due to several reasons
// 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly, marked as "not planned" - e.g., dead-end]
// 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx])
- // 3) Many tools (like docfx) don't support the new syntax yet and it isn't clear if they will in the future.
+ // 3) Many external tools don't support the new syntax yet and it isn't clear if they will in the future.
// 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]).
//
// Bottom line it's a good idea with an incomplete implementation lacking support
@@ -21,8 +21,21 @@ namespace Ubiquity.NET.Extensions
/// They also serve to provide validation when using body expressions for property
/// method implementations etc... Though the C# 14 field keyword makes that
/// use mostly a moot point.
+ ///
+ /// In .NET Standard 2.0 builds this can create ambiguities with the static extensions
+ /// in `PolyFillExceptionValidators`. This is becuase they are "Poly Filled"
+ /// in downstream versions and the resolution rules for extensions in the C# language.
+ /// Instance methods are resolved before the static extensions and therefore the extensions
+ /// here are resolved even if there is a direct static extensions. This seems broken, but
+ /// is how the language is resolving things. Therefore carefull use of namespace usings
+ /// and global usings as well as explicit use of this type is needed to resolve this. It
+ /// is NOT recommended to use explict references to the static method in `PolyFillExceptionValidators`
+ /// as the methods don't exist if the BCL type contains the method already in a given
+ /// runtime. Thus, in compilation units, needing both namespaces only this one is
+ /// explicitly referenced.
+ ///
///
- public static class FluentValidationExtensions
+ public static class ExceptionValidationExtensions
{
/// Throws an exception if is
/// Type of reference parameter to test for
diff --git a/src/Ubiquity.NET.Extensions/FluentValidation/ReadMe.md b/src/Ubiquity.NET.Extensions/FluentValidation/ReadMe.md
new file mode 100644
index 000000000..da2006705
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/FluentValidation/ReadMe.md
@@ -0,0 +1,11 @@
+# About
+This folder contains fluent validation extensions. It is a distinct namespace to aid in
+diasambiguation when using downlevel polyfills for static validation extensions. When both
+instance extensions and static extensions are available with the same name there is an
+ambiguity and the compiler resolves to the instance extension. Thus,
+`.` is resolved as
+`.` when both are available. Thus, when supporting
+downlevel runtimes (like for a Roslyn Source generator/analyzer/fixer or VSIX extension)
+then both namespaces are not implicitly "used". Instead only one is. If both namespaces are
+needed in a compilation unit, then the poly fill is "used" and the fluent form is explicitly
+referenced. Thus, there is no implicit ambiguity/confusion.
diff --git a/src/Ubiquity.NET.Extensions/GlobalNamespaceImports.cs b/src/Ubiquity.NET.Extensions/GlobalNamespaceImports.cs
index 6302a74f7..2ca3f6006 100644
--- a/src/Ubiquity.NET.Extensions/GlobalNamespaceImports.cs
+++ b/src/Ubiquity.NET.Extensions/GlobalNamespaceImports.cs
@@ -12,6 +12,11 @@ set of namespaces that is NOT consistent or controlled by the developer. THAT is
For an explanation of the benefits of the language feature see: https://www.hanselman.com/blog/implicit-usings-in-net-6
*/
+// BUG: False positive from IDE0005 - Using directive is unnecessary
+// Attempts to remove/sort are at least able to figure it out and do the right thing.
+// Bug seems to be related to multi-targetting.
+#pragma warning disable IDE0005
+
global using System;
global using System.Collections;
global using System.Collections.Generic;
@@ -23,4 +28,11 @@ set of namespaces that is NOT consistent or controlled by the developer. THAT is
global using System.IO;
global using System.Reflection;
global using System.Runtime.CompilerServices;
+global using System.Text;
global using System.Threading;
+
+global using Ubiquity.NET.Extensions;
+global using Ubiquity.NET.Extensions.Properties;
+
+// alias allows simpler porting of polyfill from .NET sources
+global using SR = Ubiquity.NET.Extensions.Properties.Resources;
diff --git a/src/Ubiquity.NET.Extensions/PolyFillExceptionValidators.cs b/src/Ubiquity.NET.Extensions/PolyFillExceptionValidators.cs
new file mode 100644
index 000000000..7f4c3250a
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/PolyFillExceptionValidators.cs
@@ -0,0 +1,206 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+// .NET 7 added the various exception static methods for parameter validation
+// This will back fill them for earlier versions.
+//
+// NOTE: C #14 extension keyword support is required to make this work.
+#if !NET7_0_OR_GREATER
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+
+namespace System
+{
+ /// poly fill extensions for static methods added in .NET 7
+ ///
+ /// This requires support of the C#14 keyword `extension` to work properly. There is
+ /// no other way to add static methods to non-partial types for source compatibility.
+ /// Otherwise code cannot use the modern .NET runtime implementations and instead
+ /// must always use some extension methods, or litter around a LOT of #if/#else/#endif
+ /// based on the framework version...
+ ///
+ [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "Extension - Broken analyzer" )]
+ [SuppressMessage( "Naming", "CA1708:Identifiers should differ by more than case", Justification = "Extension - broken analyzer" )]
+ public static class PolyFillExceptionValidators
+ {
+ /// Poly fill Extensions for
+ extension( ArgumentException )
+ {
+ /// Throw an if a string is m empty, or all whitepsace.
+ /// input string to test
+ /// expression or name of the string to test; normally provided by compiler
+ /// string is m empty, or all whitepsace
+ public static void ThrowIfNullOrWhiteSpace(
+ [NotNull] string? argument,
+ [CallerArgumentExpression( nameof( argument ) )] string? paramName = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(argument, paramName);
+
+ // argument is non-null verified by this, sadly older frameworks don't have
+ // attributes to declare that.
+ if(string.IsNullOrWhiteSpace( argument ))
+ {
+ throw new ArgumentException( SR.Argument_EmptyOrWhiteSpaceString, paramName );
+ }
+ }
+ }
+
+ /// Poly fill Extensions for
+ extension( ArgumentNullException )
+ {
+ /// Throws an aexception if the tested argument is
+ /// value to test
+ /// expression for the name of the value; normally provided by compiler
+ /// is
+ public static void ThrowIfNull(
+ [NotNull] object? argument,
+ [CallerArgumentExpression( nameof( argument ) )] string? paramName = default
+ )
+ {
+ if(argument is null)
+ {
+ throw new ArgumentNullException( paramName );
+ }
+ }
+ }
+
+ /// Poly fill Extensions for
+ extension( ObjectDisposedException )
+ {
+ /// Throws an if is .
+ /// Condition to determine if the instance is disposed
+ /// instance that is tested; Used to get type name for exception
+ /// is
+ public static void ThrowIf(
+ [DoesNotReturnIf( true )] bool condition,
+ object instance
+ )
+ {
+ if(condition)
+ {
+ throw new ObjectDisposedException( instance?.GetType().FullName );
+ }
+ }
+ }
+
+ /// Poly fill Extensions for
+ extension( ArgumentOutOfRangeException )
+ {
+ /// Throws an if is equal to .
+ /// The argument to validate as not equal to .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfEqual( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IEquatable?
+ {
+ if(EqualityComparer.Default.Equals( value, other ))
+ {
+ ThrowEqual( value, other, paramName );
+ }
+ }
+
+ /// Throws an if is not equal to .
+ /// The argument to validate as equal to .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfNotEqual( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IEquatable?
+ {
+ if(!EqualityComparer.Default.Equals( value, other ))
+ {
+ ThrowNotEqual( value, other, paramName );
+ }
+ }
+
+ /// Throws an if is greater than .
+ /// The argument to validate as less or equal than .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfGreaterThan( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IComparable
+ {
+ if(value.CompareTo( other ) > 0)
+ {
+ ThrowGreater( value, other, paramName );
+ }
+ }
+
+ /// Throws an if is greater than or equal .
+ /// The argument to validate as less than .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfGreaterThanOrEqual( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IComparable
+ {
+ if(value.CompareTo( other ) >= 0)
+ {
+ ThrowGreaterEqual( value, other, paramName );
+ }
+ }
+
+ /// Throws an if is less than .
+ /// The argument to validate as greatar than or equal than .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfLessThan( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IComparable
+ {
+ if(value.CompareTo( other ) < 0)
+ {
+ ThrowLess( value, other, paramName );
+ }
+ }
+
+ /// Throws an if is less than or equal .
+ /// The argument to validate as greatar than than .
+ /// The value to compare with .
+ /// The name of the parameter with which corresponds.
+ public static void ThrowIfLessThanOrEqual( T value, T other, [CallerArgumentExpression( nameof( value ) )] string? paramName = null )
+ where T : IComparable
+ {
+ if(value.CompareTo( other ) <= 0)
+ {
+ ThrowLessEqual( value, other, paramName );
+ }
+ }
+
+ [DoesNotReturn]
+ private static void ThrowZero( T value, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeNonZero, paramName, value ) );
+
+ [DoesNotReturn]
+ private static void ThrowNegative( T value, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeNonNegative, paramName, value ) );
+
+ [DoesNotReturn]
+ private static void ThrowNegativeOrZero( T value, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero, paramName, value ) );
+
+ [DoesNotReturn]
+ private static void ThrowGreater( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeLessOrEqual, paramName, value, other ) );
+
+ [DoesNotReturn]
+ private static void ThrowGreaterEqual( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeLess, paramName, value, other ) );
+
+ [DoesNotReturn]
+ private static void ThrowLess( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeGreaterOrEqual, paramName, value, other ) );
+
+ [DoesNotReturn]
+ private static void ThrowLessEqual( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeGreater, paramName, value, other ) );
+
+ [DoesNotReturn]
+ private static void ThrowEqual( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeNotEqual, paramName, (object?)value ?? "null", (object?)other ?? "null" ) );
+
+ [DoesNotReturn]
+ private static void ThrowNotEqual( T value, T other, string? paramName ) =>
+ throw new ArgumentOutOfRangeException( paramName, value, SR.Format( SR.ArgumentOutOfRange_Generic_MustBeEqual, paramName, (object?)value ?? "null", (object?)other ?? "null" ) );
+ }
+ }
+}
+#endif
diff --git a/src/Ubiquity.NET.Extensions/PolyFillOperatingSystem.cs b/src/Ubiquity.NET.Extensions/PolyFillOperatingSystem.cs
new file mode 100644
index 000000000..8ddbb6e49
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/PolyFillOperatingSystem.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+#if !NET5_0_OR_GREATER
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+
+namespace System
+{
+ /// Poly fill extensions for
+ ///
+ [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "Extension" )]
+ public static class PolyFillOperatingSystem
+ {
+ /// Poly fill Extensions for
+ extension( OperatingSystem )
+ {
+ /// Indicates whether the current application is running on Windows.
+ /// if the current application is running on Windows; otherwise.
+ public static bool IsWindows( )
+ {
+ return Environment.OSVersion.Platform switch
+ {
+ PlatformID.Win32S or
+ PlatformID.Win32Windows or
+ PlatformID.Win32NT or
+ PlatformID.WinCE => true,
+ _ => false,
+ };
+ }
+
+ // other forms of Is* are more difficult to poly fill as Linux, macOS, iOS, and android, are all apparently reported as PlatformId.Unix
+ // So they need to rely on additional native interop APIs for unix AND type name searches
+ // see: https://github.com/ryancheung/PlatformUtil/blob/master/PlatformUtil/PlatformInfo.cs
+ }
+ }
+}
+#endif
diff --git a/src/Ubiquity.NET.Extensions/PolyFillStringExtensions.cs b/src/Ubiquity.NET.Extensions/PolyFillStringExtensions.cs
new file mode 100644
index 000000000..b7f38dbff
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/PolyFillStringExtensions.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+#if !NET6_0_OR_GREATER
+
+// from .NET sources
+// see: https://github.com/dotnet/runtime/blob/1d1bf92fcf43aa6981804dc53c5174445069c9e4/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable SA1600 // Elements should be documented [Duplicate of CS1591]
+
+using System.Text.RegularExpressions;
+
+namespace System.Text
+{
+ /// Pollyfill extensions for support not present in older runtimes
+ ///
+ public static class PolyFillStringExtensions
+ {
+ /// Replace line endings in the string with environment specific forms
+ /// string to change line endings for
+ /// string with environment specific line endings
+ [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Extension" )]
+ public static string ReplaceLineEndings(this string self) => ReplaceLineEndings(self, Environment.NewLine);
+
+ // This is NOT the most performant implementation, it's going for simplistic pollyfill that has
+ // the correct behavior, even if not the most performant. If performance is critical, use a
+ // later version of the runtime!
+
+ /// Replace line endings in the string with a given string
+ /// string to change line endings for
+ /// Text to replace all of the line endings in
+ /// string with line endings replaced by
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Extension" )]
+ public static string ReplaceLineEndings( this string self, string replacementText )
+ {
+ ArgumentNullException.ThrowIfNull(self);
+ ArgumentNullException.ThrowIfNull(replacementText);
+
+ string retVal = UnicodeNewLinesRegEx.Replace(self, replacementText);
+
+ // if the result of replacement is the same, just return the original
+ // This is wasted overhead, but at least matches the behavior
+ return self == retVal ? self : retVal;
+ }
+
+ // The Unicode Standard, Sec. 5.8, Recommendation R4 and Table 5-2 state that the CR, LF,
+ // CRLF, NEL, LS, FF, and PS sequences are considered newline functions. That section
+ // also specifically excludes VT from the list of newline functions, so we do not include
+ // it in the regular expression match.
+
+ // language=regex
+ private const string UnicodeNewLinesRegExPattern = @"(\r\n|\r|\n|\f|\u0085|\u2028|\u2029)";
+
+ private static Regex UnicodeNewLinesRegEx { get; } = new Regex( UnicodeNewLinesRegExPattern );
+ }
+}
+#endif
diff --git a/src/Ubiquity.NET.Extensions/ProcessInfo.cs b/src/Ubiquity.NET.Extensions/ProcessInfo.cs
index 55ba51ef8..8e51ea05d 100644
--- a/src/Ubiquity.NET.Extensions/ProcessInfo.cs
+++ b/src/Ubiquity.NET.Extensions/ProcessInfo.cs
@@ -18,7 +18,10 @@ public static class ProcessInfo
public static string ExecutablePath => Environment.GetCommandLineArgs()[ 0 ];
/// Gets the name of the executable for this instance of an application
- /// This is a short hand for Path.GetFileNameWithoutExtension( )
+ ///
+ /// This is a short hand for using
+ /// as the path.
+ ///
public static string ExecutableName => Path.GetFileNameWithoutExtension( ExecutablePath );
}
}
diff --git a/src/Ubiquity.NET.Extensions/Properties/Resources.Designer.cs b/src/Ubiquity.NET.Extensions/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..039eb065f
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/Properties/Resources.Designer.cs
@@ -0,0 +1,153 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Ubiquity.NET.Extensions.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal partial class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Ubiquity.NET.Extensions.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The value cannot be an empty string or composed entirely of whitespace..
+ ///
+ internal static string Argument_EmptyOrWhiteSpaceString {
+ get {
+ return ResourceManager.GetString("Argument_EmptyOrWhiteSpaceString", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be equal to '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeEqual {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeEqual", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be greater than '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeGreater {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeGreater", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be greater than or equal to '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeGreaterOrEqual {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeGreaterOrEqual", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be less than '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeLess {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeLess", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be less than or equal to '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeLessOrEqual {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeLessOrEqual", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be a non-negative value..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeNonNegative {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeNonNegative", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be a non-negative and non-zero value..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must be a non-zero value..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeNonZero {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeNonZero", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} ('{1}') must not be equal to '{2}'..
+ ///
+ internal static string ArgumentOutOfRange_Generic_MustBeNotEqual {
+ get {
+ return ResourceManager.GetString("ArgumentOutOfRange_Generic_MustBeNotEqual", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions/Properties/Resources.resx b/src/Ubiquity.NET.Extensions/Properties/Resources.resx
new file mode 100644
index 000000000..68a149afe
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/Properties/Resources.resx
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ {0} ('{1}') must be a non-zero value.
+
+
+ {0} ('{1}') must be a non-negative value.
+
+
+ {0} ('{1}') must be a non-negative and non-zero value.
+
+
+ {0} ('{1}') must be less than or equal to '{2}'.
+
+
+ {0} ('{1}') must be less than '{2}'.
+
+
+ {0} ('{1}') must be greater than or equal to '{2}'.
+
+
+ {0} ('{1}') must be greater than '{2}'.
+
+
+ {0} ('{1}') must be equal to '{2}'.
+
+
+ {0} ('{1}') must not be equal to '{2}'.
+
+
+ The value cannot be an empty string or composed entirely of whitespace.
+
+
diff --git a/src/Ubiquity.NET.Extensions/Properties/StringResourceExtensions.cs b/src/Ubiquity.NET.Extensions/Properties/StringResourceExtensions.cs
new file mode 100644
index 000000000..f3bdfada4
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/Properties/StringResourceExtensions.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using Ubiquity.NET.Extensions.FluentValidation;
+
+namespace Ubiquity.NET.Extensions.Properties
+{
+ internal static class StringResourceExtensions
+ {
+ // RESX file generator does not use `partial` so C# 14 extension is the only option...
+ extension(Ubiquity.NET.Extensions.Properties.Resources)
+ {
+ internal static string Format( [NotNull][StringSyntax(StringSyntaxAttribute.CompositeFormat)] string fmt, TArg0 arg0 )
+ {
+ fmt.ThrowIfNull();
+ return string.Format( CultureInfo.CurrentCulture, fmt, arg0 );
+ }
+
+ internal static string Format( [NotNull][StringSyntax( StringSyntaxAttribute.CompositeFormat )] string fmt, TArg0 arg0, TArg1 arg1 )
+ {
+ fmt.ThrowIfNull();
+ return string.Format( CultureInfo.CurrentCulture, fmt, arg0, arg1 );
+ }
+
+ internal static string Format( [NotNull][StringSyntax( StringSyntaxAttribute.CompositeFormat )] string fmt, TArg0 arg0, TArg1 arg1, TArg3 arg3 )
+ {
+ fmt.ThrowIfNull();
+ return string.Format( CultureInfo.CurrentCulture, fmt, arg0, arg1, arg3 );
+ }
+ }
+
+#if NET7_0_OR_GREATER
+ // This is just a utility method, not an extension
+ internal static CompositeFormat ParseAsFormat( [NotNull][StringSyntax( StringSyntaxAttribute.CompositeFormat )] this string? self )
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace( self );
+ return CompositeFormat.Parse( self );
+ }
+#endif
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions/SourceRange.cs b/src/Ubiquity.NET.Extensions/SourceRange.cs
index 82f48aaae..5a5544a54 100644
--- a/src/Ubiquity.NET.Extensions/SourceRange.cs
+++ b/src/Ubiquity.NET.Extensions/SourceRange.cs
@@ -94,6 +94,7 @@ private string FormatMsBuild( IFormatProvider formatProvider )
return string.Empty;
}
+#if NET6_0_OR_GREATER
if(End.Line == 0)
{
return Start.Column == 0
@@ -110,6 +111,24 @@ private string FormatMsBuild( IFormatProvider formatProvider )
? string.Create(formatProvider, $"({Start.Line}-{End.Line})")
: string.Create(formatProvider, $"({Start.Line}, {Start.Column}, {End.Line}, {End.Column})");
}
+#else
+ if(End.Line == 0)
+ {
+ return Start.Column == 0
+ ? string.Format( formatProvider, "({0})", Start.Line)
+ : string.Format( formatProvider, "({0}, {1})", Start.Line, Start.Column );
+ }
+ else if(End.Line == Start.Line)
+ {
+ return string.Format( formatProvider, "({0}, {1}-{2})", Start.Line, Start.Column, End.Column );
+ }
+ else
+ {
+ return Start.Column == 0 && End.Column == 0
+ ? string.Format( formatProvider, "({0}-{1})", Start.Line, End.Line )
+ : string.Format( formatProvider, "({0}, {1}, {2}, {3})", Start.Line, Start.Column, End.Line, End.Column );
+ }
+#endif
}
[SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Place holder for future work" )]
diff --git a/src/Ubiquity.NET.Extensions/StringNormalizer.cs b/src/Ubiquity.NET.Extensions/StringNormalizer.cs
index cb69cddb2..ac2d5d975 100644
--- a/src/Ubiquity.NET.Extensions/StringNormalizer.cs
+++ b/src/Ubiquity.NET.Extensions/StringNormalizer.cs
@@ -26,6 +26,7 @@ public enum LineEndingKind
CarriageReturn,
}
+#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
/// Utility class for converting line endings to expected forms
/// This is similar to and
/// except that it allows explicit
@@ -33,6 +34,7 @@ public enum LineEndingKind
/// Ultimately all forms of normalization resolves to a call to
/// .
///
+#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
public static partial class StringNormalizer
{
/// Gets a string form of the line ending
@@ -68,16 +70,18 @@ public static string LineEnding( this LineEndingKind kind )
return txt.NormalizeLineEndings( LineEndingKind.MixedOrUnknownEndings, dstKind );
}
+#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
/// Converts a string into a string with managed environment line endings
/// string to convert
/// Line ending kind for the source ()
/// Line ending kind for the destination (return string)
/// Normalized string; If the is the same as this is returns un-modified
///
- /// Unlike the runtime provided this does NOT replace ALL forms
- /// of line endings unless is . In
+ /// Unlike the .NET 6+ runtime provided this does NOT replace
+ /// ALL forms of line endings unless is . In
/// all other cases it ONLY replaces exact matches for the line endings specified in .
///
+#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
[return: NotNullIfNotNull(nameof(txt))]
[SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Nested conditional are NOT simpler" )]
public static string? NormalizeLineEndings( this string? txt, LineEndingKind srcKind, LineEndingKind dstKind )
@@ -93,9 +97,15 @@ public static string LineEnding( this LineEndingKind kind )
return txt;
}
+#if NETSTANDARD2_0
+ return srcKind == LineEndingKind.MixedOrUnknownEndings
+ ? txt!.ReplaceLineEndings( dstKind.LineEnding() )
+ : txt!.Replace( srcKind.LineEnding(), dstKind.LineEnding() );
+#else
return srcKind == LineEndingKind.MixedOrUnknownEndings
? txt.ReplaceLineEndings( dstKind.LineEnding() )
: txt.Replace( srcKind.LineEnding(), dstKind.LineEnding(), StringComparison.Ordinal );
+#endif
}
// simplifies consistency of exception in face of unknown environment configuration
diff --git a/src/Ubiquity.NET.Extensions/StringSplitOptions2.cs b/src/Ubiquity.NET.Extensions/StringSplitOptions2.cs
new file mode 100644
index 000000000..db06a21f4
--- /dev/null
+++ b/src/Ubiquity.NET.Extensions/StringSplitOptions2.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.Extensions
+{
+ /// Poly fill extensions for the enumeration
+ [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "Extension - Broken analyzer" )]
+ [Flags]
+ public enum StringSplitOptions2
+ {
+ /// Use the default options when splitting strings.
+ None = StringSplitOptions.None,
+
+ /// Omit array elements that contain an empty string from the result.
+ ///
+ /// If and are specified together,
+ /// then substrings that consist only of white-space characters are also removed from the result.
+ ///
+ RemoveEmptyEntries = StringSplitOptions.RemoveEmptyEntries,
+
+#if NET5_0_OR_GREATER
+ /// Trim white-space characters from each substring in the result.
+ ///
+ /// If and are specified together,
+ /// then substrings that consist only of white-space characters are also removed from the result.
+ ///
+ TrimEntries = StringSplitOptions.TrimEntries
+#else
+ /// Trim white-space characters from each substring in the result.
+ ///
+ ///
+ /// The official value of this field is available in .NET 5 and later versions only. Unless a method
+ /// using is explicitily re-written to support ,
+ /// then the functionality for this is not available.
+ ///
+ ///
+ /// If and are specified together,
+ /// then substrings that consist only of white-space characters are also removed from the result.
+ ///
+ ///
+ TrimEntries = 2,
+#endif
+ }
+}
diff --git a/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj b/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
index f14caee5e..23b7f2797 100644
--- a/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
+++ b/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net8.0;netstandard2.0
preview
enable
@@ -22,6 +22,7 @@
Apache-2.0 WITH LLVM-exception
true
snupkg
+ en-US
@@ -29,6 +30,20 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
all
@@ -37,4 +52,18 @@
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
diff --git a/src/Ubiquity.NET.InteropHelpers.UT/AssemblyInfo.cs b/src/Ubiquity.NET.InteropHelpers.UT/AssemblyInfo.cs
index 38f948ad7..4e002590c 100644
--- a/src/Ubiquity.NET.InteropHelpers.UT/AssemblyInfo.cs
+++ b/src/Ubiquity.NET.InteropHelpers.UT/AssemblyInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -22,3 +23,4 @@
[assembly: CLSCompliant( false )]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Ubiquity.NET.Llvm.JIT.Tests/Properties/AssemblyInfo.cs b/src/Ubiquity.NET.Llvm.JIT.Tests/Properties/AssemblyInfo.cs
index 10c931311..79cf6d2df 100644
--- a/src/Ubiquity.NET.Llvm.JIT.Tests/Properties/AssemblyInfo.cs
+++ b/src/Ubiquity.NET.Llvm.JIT.Tests/Properties/AssemblyInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -17,3 +18,4 @@
[assembly: CLSCompliant( false )]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Ubiquity.NET.Llvm.Tests/Properties/AssemblyInfo.cs b/src/Ubiquity.NET.Llvm.Tests/Properties/AssemblyInfo.cs
index 10c931311..79cf6d2df 100644
--- a/src/Ubiquity.NET.Llvm.Tests/Properties/AssemblyInfo.cs
+++ b/src/Ubiquity.NET.Llvm.Tests/Properties/AssemblyInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -17,3 +18,4 @@
[assembly: CLSCompliant( false )]
[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+[assembly: ExcludeFromCodeCoverage]
diff --git a/src/Ubiquity.NET.Llvm.slnx b/src/Ubiquity.NET.Llvm.slnx
index 0b3286a4d..e95f8bfbf 100644
--- a/src/Ubiquity.NET.Llvm.slnx
+++ b/src/Ubiquity.NET.Llvm.slnx
@@ -80,6 +80,7 @@
+
diff --git a/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs b/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs
index e57085846..021582df5 100644
--- a/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs
+++ b/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs
@@ -32,8 +32,8 @@ where it belongs.
global using System.Security.Cryptography;
global using System.Text;
global using System.Threading;
-
global using Ubiquity.NET.Extensions;
+global using Ubiquity.NET.Extensions.FluentValidation;
global using Ubiquity.NET.InteropHelpers;
global using Ubiquity.NET.Llvm.DebugInfo;
global using Ubiquity.NET.Llvm.Instructions;
diff --git a/src/Ubiquity.NET.Runtime.Utils/ReadMe.md b/src/Ubiquity.NET.Runtime.Utils/ReadMe.md
index c93350026..388e99770 100644
--- a/src/Ubiquity.NET.Runtime.Utils/ReadMe.md
+++ b/src/Ubiquity.NET.Runtime.Utils/ReadMe.md
@@ -1,7 +1,6 @@
# Ubiquity.NET.Runtime.Utils
This library contains support functionality to aid in building a language parser or runtime,
including common implementation of a Read-Evaluate-Print Loop (REPL). Generally this is used
-in conjunction with custom types and the Ubiquity.NET.Llvm library to provide custom DSL JIT
-support.
+in conjunction with the Ubiquity.NET.Llvm library to provide custom DSL JIT support.
See the Kaleidoscope tutorial for details on how to use this library
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/AssemblyInfo.cs b/src/Ubiquity.NET.SrcGeneration.UT/AssemblyInfo.cs
new file mode 100644
index 000000000..8b27dbe18
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+// In SDK-style projects such as this one, several assembly attributes that were historically
+// defined in this file are now automatically added during build and populated with
+// values defined in project properties. For details of which attributes are included
+// and how to customise this process see: https://aka.ms/assembly-info-properties
+
+// Setting ComVisible to false makes the types in this assembly not visible to COM
+// components. If you need to access a type in this assembly from COM, set the ComVisible
+// attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM.
+
+[assembly: Guid("f78531f5-3836-4e03-a1b1-59c9cac29d3d")]
+
+// Tests are so trivial they perform better when not individually parallelized.
+// Unfortunately this is an assembly wide choice and not class or method level
+// see: https://github.com/microsoft/testfx/issues/5555#issuecomment-3448956323
+[assembly: Parallelize( Scope = ExecutionScope.ClassLevel )]
+
+// can't use this at assembly level as it isn't supported there for downlevel... [Sigh...]
+//[assembly: ExcludeFromCodeCoverage]
+
+// NOTE: use of this and `internal` test classes results in a flurry of
+// error CA1812: '' is an internal class that is apparently never instantiated. If so, remove the code from the assembly.
+// If this class is intended to contain only static members, make it 'static' (Module in Visual Basic).
+// (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1812)
+// In other words, not worth the bother...
+// [assembly: DiscoverInternals]
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/CSharp/IndentedTestWriterExtensionTests.cs b/src/Ubiquity.NET.SrcGeneration.UT/CSharp/IndentedTestWriterExtensionTests.cs
new file mode 100644
index 000000000..38265370f
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/CSharp/IndentedTestWriterExtensionTests.cs
@@ -0,0 +1,387 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using Ubiquity.NET.SrcGeneration.CSharp;
+
+// simplification for self==null test cases
+using CsExtensions = Ubiquity.NET.SrcGeneration.CSharp.IndentedTextWriterExtensions;
+
+namespace Ubiquity.NET.SrcGeneration.UT.CSharp
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public class IndentedTestWriterExtensionTests
+ {
+ #region Basic API Validation
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ [TestMethod]
+ public void Methods_report_exception_with_invalid_input( )
+ {
+ var ex = Assert.ThrowsExactly(()=>CsExtensions.WriteAutoGeneratedComment(null, "tool", "version"));
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.WriteAutoGeneratedCommentBlock(null, "tool", "version");
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.Namespace(null, "My.Namespace");
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.Struct(null, "access", "StructName");
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.UnsafeScope(null);
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.Scope(null);
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) => CsExtensions.MultiLineComment( null, null ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = CsExtensions.Class(null, "access", "ClassName");
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ // additional param checks (not "self")
+ using var writer = new OwningIndentedStringWriter();
+ ex = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( null, "version" ) );
+ Assert.AreEqual( "toolName", ex.ParamName );
+
+ var argEx = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( string.Empty, "version" ) );
+ Assert.AreEqual( "toolName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( " \t ", "version" ) );
+ Assert.AreEqual( "toolName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( "tool", null ) );
+ Assert.AreEqual( "toolVersion", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( "tool", string.Empty ) );
+ Assert.AreEqual( "toolVersion", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => writer.WriteAutoGeneratedComment( "tool", " \t " ) );
+ Assert.AreEqual( "toolVersion", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( null, "version");
+ } );
+ Assert.AreEqual( "toolName", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( string.Empty, "version");
+ } );
+ Assert.AreEqual( "toolName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( " \t ", "version");
+ } );
+ Assert.AreEqual( "toolName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( "tool", null);
+ } );
+ Assert.AreEqual( "toolVersion", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( "tool", string.Empty);
+ } );
+ Assert.AreEqual( "toolVersion", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.WriteAutoGeneratedCommentBlock( "tool", " \t ");
+ } );
+ Assert.AreEqual( "toolVersion", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Namespace( null);
+ } );
+ Assert.AreEqual( "namespaceName", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Namespace( string.Empty);
+ } );
+ Assert.AreEqual( "namespaceName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Namespace( " \t ");
+ } );
+ Assert.AreEqual( "namespaceName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Struct( null, null);
+ } );
+ Assert.AreEqual( "structName", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Struct( null, string.Empty);
+ } );
+ Assert.AreEqual( "structName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Struct( null, " \t ");
+ } );
+ Assert.AreEqual( "structName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Class( null, null);
+ } );
+ Assert.AreEqual( "className", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Class(null, string.Empty);
+ } );
+ Assert.AreEqual( "className", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) =>
+ {
+ using var scope = writer.Class( null, " \t ");
+ } );
+ Assert.AreEqual( "className", argEx.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+ #endregion
+
+ [TestMethod]
+ public void WriteAutoGeneratedComment_succeeds( )
+ {
+ const string expected = """
+ // ------------------------------------------------------------------------------
+ //
+ // This code was generated by a tool.
+ // MyTool [1.2.3.4]
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ // ------------------------------------------------------------------------------
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ writer.WriteAutoGeneratedComment("MyTool", "1.2.3.4");
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void WriteAutoGeneratedComment_with_source_succeeds( )
+ {
+ const string expected = """
+ // ------------------------------------------------------------------------------
+ //
+ // This code was generated by a tool.
+ // MyTool [1.2.3.4]
+ // From: C:\SomeDir\MyFile.cs
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ // ------------------------------------------------------------------------------
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ writer.WriteAutoGeneratedComment( "MyTool", "1.2.3.4", @"C:\SomeDir\MyFile.cs" );
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteAutoGeneratedCommentBlock_succeeds( )
+ {
+ const string expected = """
+ // ------------------------------------------------------------------------------
+ //
+ // This code was generated by a tool.
+ // MyTool [1.2.3.4]
+ //
+ // This is a test
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ // ------------------------------------------------------------------------------
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.WriteAutoGeneratedCommentBlock( "MyTool", "1.2.3.4" ))
+ {
+ writer.WriteLine("//");
+ writer.WriteLine("// This is a test");
+ writer.WriteLine("//");
+ }
+
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void Namespace_succeeds( )
+ {
+ const string expected = """
+ namespace My.Namespace
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.Namespace( "My.Namespace" ))
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Struct_succeeds( )
+ {
+ const string expected = """
+ public partial struct MyStruct
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.Struct("public partial", "MyStruct" ))
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void UnsafeScope_succeeds( )
+ {
+ const string expected = """
+ unsafe
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.UnsafeScope())
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Scope_with_leading_succeeds( )
+ {
+ const string expected = """
+ // C# scope
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.Scope( "// C# scope" ))
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Scope_without_leading_succeeds( )
+ {
+ const string expected = """
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.Scope( ))
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void MultiLineComment_with_null_or_empty_text_is_nop( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ writer.MultiLineComment(null);
+ Assert.AreEqual(0, ((StringWriter)writer.InnerWriter).GetStringBuilder().Length);
+
+ writer.MultiLineComment( string.Empty );
+ Assert.AreEqual( 0, ((StringWriter)writer.InnerWriter).GetStringBuilder().Length );
+
+ writer.MultiLineComment( " \t " );
+ Assert.AreEqual( 0, ((StringWriter)writer.InnerWriter).GetStringBuilder().Length );
+ }
+
+ [TestMethod]
+ public void MultiLineComment_succeeds( )
+ {
+ const string expected = """
+ /*
+ This is a test...
+ of a multi-line comment
+ */
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ writer.MultiLineComment( string.Join(Environment.NewLine, ["This is a test...", "of a multi-line comment"] ));
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Class_succeeds( )
+ {
+ const string expected = """
+ public partial class MyClass
+ {
+ // This is a test...
+ }
+ """;
+
+ using var writer = new OwningIndentedStringWriter();
+ using(writer.Class( "public partial", "MyClass" ))
+ {
+ writer.WriteLine( "// This is a test..." );
+ }
+
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/CSharp/TextWriterExtensionsTests.cs b/src/Ubiquity.NET.SrcGeneration.UT/CSharp/TextWriterExtensionsTests.cs
new file mode 100644
index 000000000..06ffdcbad
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/CSharp/TextWriterExtensionsTests.cs
@@ -0,0 +1,250 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using Ubiquity.NET.SrcGeneration.CSharp;
+
+namespace Ubiquity.NET.SrcGeneration.UT.CSharp
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public class TextWriterExtensionsTests
+ {
+ #region Basic API Validation
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ /// Validates extension APIs handle nulls, empty strings, etc...
+ [TestMethod]
+ public void ExtensionMethods_throw_on_null_or_whitespace_args( )
+ {
+ var ex = Assert.ThrowsExactly(()=>TextWriterExtensions.WriteAttribute( null, "fooAttrib" ));
+ Assert.AreEqual( "self", ex.ParamName );
+
+ Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteAttributeLine( null, "fooAttrib" ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteRemarksComment( null, null ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteSummaryAndRemarksComments( null, null ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteSummaryComment( null, null ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteUsingDirective( null, string.Empty ) );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ using var writer = new StringWriter();
+ ex = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteSummaryAndRemarksComments( writer, "non-null remarks", null ) );
+ Assert.AreEqual( "defaultSummary", ex.ParamName );
+
+ var argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteSummaryAndRemarksComments( writer, "non-null remarks", " \t " ) );
+ Assert.AreEqual( "defaultSummary", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( () => TextWriterExtensions.WriteAttributeLine(writer, null));
+ Assert.AreEqual("attributeName", ex.ParamName);
+
+ argEx = Assert.ThrowsExactly( () => TextWriterExtensions.WriteAttributeLine(writer, string.Empty));
+ Assert.AreEqual( "attributeName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteAttributeLine( writer, " " ) );
+ Assert.AreEqual( "attributeName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( () => TextWriterExtensions.WriteAttribute(writer, null));
+ Assert.AreEqual( "attributeName", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteAttribute( writer, string.Empty ) );
+ Assert.AreEqual( "attributeName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteAttribute( writer, " " ) );
+ Assert.AreEqual( "attributeName", argEx.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteUsingDirective( writer, null ) );
+ Assert.AreEqual( "namespaceName", ex.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteUsingDirective( writer, string.Empty ) );
+ Assert.AreEqual( "namespaceName", argEx.ParamName );
+
+ argEx = Assert.ThrowsExactly( ( ) => TextWriterExtensions.WriteUsingDirective( writer, " " ) );
+ Assert.AreEqual( "namespaceName", argEx.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+ #endregion
+
+ [TestMethod]
+ public void WriteAttributeLine_with_no_args_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteAttributeLine("TestIt");
+ Assert.AreEqual("[TestIt]" + Environment.NewLine, writer.ToString());
+
+ writer.GetStringBuilder().Clear();
+ writer.WriteAttributeLine("AnotherAttribute");
+ Assert.AreEqual( "[AnotherAttribute]" + Environment.NewLine, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteAttribute_with_no_args_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteAttribute( "TestIt" );
+ Assert.AreEqual( "[TestIt]", writer.ToString() );
+
+ writer.GetStringBuilder().Clear();
+ writer.WriteAttribute( "AnotherAttribute" );
+ Assert.AreEqual( "[AnotherAttribute]", writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteAttributeLine_with_args_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteAttributeLine( "TestIt", "baz", "foo=bar" );
+ Assert.AreEqual( "[TestIt(baz, foo=bar)]" + Environment.NewLine, writer.ToString() );
+
+ writer.GetStringBuilder().Clear();
+ writer.WriteAttributeLine( "AnotherAttribute", "baz:123", "foo=bar" );
+ Assert.AreEqual( "[AnotherAttribute(baz:123, foo=bar)]" + Environment.NewLine, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteAttribute_with_args_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteAttribute( "TestIt", "baz", "foo=bar" );
+ Assert.AreEqual( "[TestIt(baz, foo=bar)]", writer.ToString() );
+
+ writer.GetStringBuilder().Clear();
+ writer.WriteAttribute( "AnotherAttribute", "baz:123", "foo=bar" );
+ Assert.AreEqual( "[AnotherAttribute(baz:123, foo=bar)]", writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteSummaryContent_with_null_or_empty_description_is_nop( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteSummaryComment( null );
+ Assert.AreEqual(0, writer.GetStringBuilder().Length);
+
+ writer.WriteSummaryComment( string.Empty );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length );
+ }
+
+ [TestMethod]
+ public void WriteSummaryContent_with_description_succeeds( )
+ {
+ // NOTE: Literal strings requires a blank line to indicate a line terminator.
+ // Otherwise, the compiler generates a string without a terminating new line!
+ const string expected = """
+ /// description of this API
+
+ """;
+
+ using var writer = new StringWriter();
+ writer.WriteSummaryComment( "description of this API" );
+ Assert.AreEqual( expected, writer.ToString() );
+
+ // ensure trimming is applied correctly
+ writer.GetStringBuilder().Clear();
+ writer.WriteSummaryComment( " \t description of this API " );
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteRemarksComment_with_null_or_empty_txt_is_nop( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteRemarksComment( null );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length );
+
+ writer.WriteRemarksComment( string.Empty );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length );
+ }
+
+ [TestMethod]
+ public void WriteRemarksComment_with_txt_succeeds( )
+ {
+ const string input = """
+ This is a remarks line
+
+
+ This is another discrete line. The preceding duplicate blank is removed.
+ """;
+
+ // NOTE: Literal strings requires a blank line to indicate a line terminator.
+ // Otherwise, the compiler generates a string without a terminating new line!
+ const string expected = """
+ ///
+ /// This is a remarks line
+ ///
+ /// This is another discrete line. The preceding duplicate blank is removed.
+ ///
+
+ """;
+
+ using var writer = new StringWriter();
+ writer.WriteRemarksComment( input );
+ Assert.AreEqual( expected, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void WriteSummaryAndRemarksComments_with_null_or_empty_txt_is_nop( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteSummaryAndRemarksComments( null );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "null should be nop" );
+
+ writer.WriteSummaryAndRemarksComments( string.Empty );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "empty string should be NOP" );
+
+ writer.WriteSummaryAndRemarksComments( " \t " );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "All whitespace should be nop" );
+ }
+
+ [TestMethod]
+ public void WriteSummaryAndRemarksComments_with_valid_inputs_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteSummaryAndRemarksComments( null );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "null should be nop" );
+
+ writer.WriteSummaryAndRemarksComments( string.Empty );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "empty string should be NOP" );
+
+ writer.WriteSummaryAndRemarksComments( " \t " );
+ Assert.AreEqual( 0, writer.GetStringBuilder().Length, "All whitespace should be nop" );
+
+ const string inputRemarks = """
+ This is a remarks line
+
+
+ This is another discrete line. The preceding duplicate blank is removed.
+ """;
+
+ const string defaultSummary = " \t description of this API ";
+
+ writer.WriteSummaryAndRemarksComments(inputRemarks, defaultSummary);
+
+ // NOTE: Literal strings requires a blank line to indicate a line terminator.
+ // Otherwise, the compiler generates a string without a terminating new line!
+ const string expected = """
+ /// description of this API
+ ///
+ /// This is a remarks line
+ ///
+ /// This is another discrete line. The preceding duplicate blank is removed.
+ ///
+
+ """;
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void WriteUsingDirective_succeeds( )
+ {
+ using var writer = new StringWriter();
+ writer.WriteUsingDirective( "My.Namespace" );
+ string expected = "using My.Namespace;" + Environment.NewLine;
+ Assert.AreEqual(expected, writer.ToString());
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/GlobalNamespaceImports.cs b/src/Ubiquity.NET.SrcGeneration.UT/GlobalNamespaceImports.cs
new file mode 100644
index 000000000..edc70b57e
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/GlobalNamespaceImports.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+/*
+NOTE:
+While the MsBuild `ImplicitUsings` property is banned from this repo, the C# language feature of global usings is NOT.
+The build property will auto include an invisible and undiscoverable (without looking up obscure documentation)
+set of namespaces that is NOT consistent or controlled by the developer. THAT is what is BAD/BROKEN about that feature.
+By banning it's use and then providing a `GlobalNamespaceImports.cs` source file with ONLY global using statements ALL of
+that is eliminated. Such use of the language feature restores FULL control and visibility of the namespaces to the developer,
+where it belongs. For a good explanation of this problem see: https://rehansaeed.com/the-problem-with-csharp-10-implicit-usings/.
+For an explanation of the benefits of the language feature see: https://www.hanselman.com/blog/implicit-usings-in-net-6
+*/
+
+global using System;
+global using System.CodeDom.Compiler;
+global using System.Collections.Generic;
+global using System.Diagnostics.CodeAnalysis;
+global using System.Globalization;
+global using System.IO;
+
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+global using Ubiquity.NET.Extensions;
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/GlobalSuppressions.cs b/src/Ubiquity.NET.SrcGeneration.UT/GlobalSuppressions.cs
new file mode 100644
index 000000000..9871da584
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/GlobalSuppressions.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test assembly" )]
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/IndentedTextWriterExtensionsTests.cs b/src/Ubiquity.NET.SrcGeneration.UT/IndentedTextWriterExtensionsTests.cs
new file mode 100644
index 000000000..895b3570f
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/IndentedTextWriterExtensionsTests.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.SrcGeneration.UT
+{
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public sealed class IndentedTextWriterExtensionsTests
+ {
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ // nullability checks is the point of this test...
+ [TestMethod]
+ public void WriteEmptyLine_throws_if_null( )
+ {
+ var ex = Assert.ThrowsExactly(()=>IndentedTextWriterExtensions.WriteEmptyLine(null));
+ Assert.AreEqual( "self", ex.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void WriteEmptyLine_writes_a_blank_line_without_indentation( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ writer.WriteLine("line1");
+ IndentedTextWriterExtensions.WriteEmptyLine(writer);
+ writer.Write("line2 [no trailing newline]");
+ Assert.AreEqual( TestEmptyLine, writer.ToString() );
+ }
+
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ // nullability checks is the point of this test...
+ [TestMethod]
+ public void PushIndent_throws_if_null( )
+ {
+ var ex = Assert.ThrowsExactly(()=>
+ {
+ using var x = IndentedTextWriterExtensions.PushIndent(null);
+ });
+ Assert.AreEqual( "self", ex.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void PushIndent_correctly_pushes_indentation_level( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ Assert.AreEqual( 0, writer.Indent, "SANITY: Writer should start at level 0" );
+ using(var scope1 = IndentedTextWriterExtensions.PushIndent( writer ))
+ {
+ Assert.AreEqual( 1, writer.Indent, "Indentation level should be +1 while indented" );
+ using(var scope2 = IndentedTextWriterExtensions.PushIndent( writer ))
+ {
+ Assert.AreEqual( 2, writer.Indent, "Indentation level should be +1 while indented" );
+ }
+
+ Assert.AreEqual( 1, writer.Indent, "Indentation level should be -1 after indentation" );
+ }
+
+ Assert.AreEqual( 0, writer.Indent, "Disposal of scope should result in indentation level of 0" );
+ }
+
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ // nullability checks is the point of this test...
+ [TestMethod]
+ public void Block_throws_if_null( )
+ {
+ var ex = Assert.ThrowsExactly(()=>
+ {
+ using var x = IndentedTextWriterExtensions.Block(null, "{","}");
+ });
+ Assert.AreEqual( "self", ex.ParamName );
+
+ using var writer = new OwningIndentedStringWriter();
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var x = IndentedTextWriterExtensions.Block(writer, null,"}");
+ } );
+
+ Assert.AreEqual( "open", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ using var x = IndentedTextWriterExtensions.Block(writer, "{",null);
+ } );
+
+ Assert.AreEqual( "close", ex.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void Block_produces_correct_results_for_defaults( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ using(var scope = IndentedTextWriterExtensions.Block( writer, "begin", "end" ))
+ {
+ writer.WriteLine( "test line" );
+ }
+
+ Assert.AreEqual( 0, writer.Indent );
+ Assert.AreEqual( TestBlock, writer.ToString());
+ }
+
+ [TestMethod]
+ public void Block_produces_correct_results_with_leading_line( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ using(var scope = IndentedTextWriterExtensions.Block( writer, "begin", "end", "" ))
+ {
+ writer.WriteLine( "test line" );
+ }
+
+ Assert.AreEqual( 0, writer.Indent );
+ Assert.AreEqual( TestBlockWithLeadingLine, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Block_produces_correct_results_with_leading_line_no_indentation( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ using(var scope = IndentedTextWriterExtensions.Block( writer, "begin", "end", "", indented: false ))
+ {
+ writer.WriteLine( "test line" );
+ }
+
+ Assert.AreEqual( 0, writer.Indent );
+ Assert.AreEqual( TestBlockWithLeadingLineNoIndentation, writer.ToString() );
+ }
+
+ [TestMethod]
+ public void Block_produces_correct_results_with_no_indentation( )
+ {
+ using var writer = new OwningIndentedStringWriter();
+ using(var scope = IndentedTextWriterExtensions.Block( writer, "begin", "end", indented: false ))
+ {
+ writer.WriteLine( "test line" );
+ }
+
+ Assert.AreEqual( 0, writer.Indent );
+ Assert.AreEqual( TestBlockWithNoIndentation, writer.ToString() );
+ }
+
+ private const string TestBlock = """
+ begin
+ test line
+ end
+ """;
+
+ private const string TestBlockWithLeadingLine = """
+
+ begin
+ test line
+ end
+ """;
+
+ private const string TestBlockWithLeadingLineNoIndentation = """
+
+ begin
+ test line
+ end
+ """;
+
+ private const string TestBlockWithNoIndentation = """
+ begin
+ test line
+ end
+ """;
+
+ private const string TestEmptyLine = """
+ line1
+
+ line2 [no trailing newline]
+ """;
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/OwningIndentedStringWriter.cs b/src/Ubiquity.NET.SrcGeneration.UT/OwningIndentedStringWriter.cs
new file mode 100644
index 000000000..d874130ea
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/OwningIndentedStringWriter.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.SrcGeneration.UT
+{
+ // simple owning indented writer using a StringWriter, this is NOT generalized due to the potential
+ // confusion over the ownership when an exception occurs during construction of this type. (It isn't
+ // actually moved in such a case - but safe/correct handling of that in general is rather complicated.)
+ // In a test scenario, an exception in the constructor will crash the test host and treated as a test
+ // failure. This is desired behavior, and extremely unlikely to ever occur, so OK in this special case.
+ [SuppressMessage( "StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "DUH, it's file scoped" )]
+ [ExcludeFromCodeCoverage]
+ internal class OwningIndentedStringWriter
+ : IndentedTextWriter
+ {
+ [SuppressMessage( "Reliability", "CA2000:Dispose objects before losing scope", Justification = "Owned, move semantics, disposed of in Dispose" )]
+ public OwningIndentedStringWriter( string? tabString = null )
+ : base( new StringWriter( CultureInfo.CurrentCulture ), tabString ?? DefaultTabString )
+ {
+ }
+
+ protected override void Dispose( bool disposing )
+ {
+ if(disposing)
+ {
+ InnerWriter.Dispose();
+ }
+
+ base.Dispose( disposing );
+ }
+
+ public override string? ToString( )
+ {
+ return InnerWriter.ToString();
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/StringExtensionTests.cs b/src/Ubiquity.NET.SrcGeneration.UT/StringExtensionTests.cs
new file mode 100644
index 000000000..8175fdaa8
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/StringExtensionTests.cs
@@ -0,0 +1,303 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+
+namespace Ubiquity.NET.SrcGeneration.UT
+{
+ [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Record" )]
+ [ExcludeFromCodeCoverage]
+ internal readonly record struct LineData( string Input, ImmutableArray Expected );
+
+ [TestClass]
+ [ExcludeFromCodeCoverage]
+ public class StringExtensionTests
+ {
+ public TestContext TestContext { get; set; }
+
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ // tests are intended to VALIDATE nullability checks in implementation
+ [TestMethod]
+ public void Extensions_throw_if_null_self( )
+ {
+ var ex = Assert.ThrowsExactly(()=>
+ {
+ StringExtensions.GetCommentLines( null );
+ } );
+ Assert.AreEqual("self", ex.ParamName);
+
+ ex = Assert.ThrowsExactly(()=>
+ {
+ StringExtensions.EscapeComment( null );
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ StringExtensions.SplitLines( null );
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ StringExtensions.MakeXmlSafe( null );
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ StringExtensions.EscapeForComment( null );
+ } );
+ Assert.AreEqual( "self", ex.ParamName );
+
+ ex = Assert.ThrowsExactly( ( ) =>
+ {
+ var strings = StringExtensions.SkipDuplicates( null );
+ } );
+
+ Assert.AreEqual( "self", ex.ParamName );
+ }
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ [TestMethod]
+ public void GetCommentLines_splits_string_correctly( )
+ {
+ // default behavior is StringSplitOptions2.TrimEntries - verify that
+ var testData = GetCommentLinesTestData( StringSplitOptions2.TrimEntries );
+ var actual = StringExtensions.GetCommentLines(testData.Input).ToImmutableArray();
+ VerifyLineData( testData.Expected, actual );
+ }
+
+ [TestMethod]
+ [DataRow( StringSplitOptions2.None )]
+ [DataRow( StringSplitOptions2.RemoveEmptyEntries )]
+ [DataRow( StringSplitOptions2.TrimEntries )]
+ [DataRow( StringSplitOptions2.TrimEntries | StringSplitOptions2.RemoveEmptyEntries )]
+ public void GetCommentLines_with_options( StringSplitOptions2 options )
+ {
+ var testData = GetCommentLinesTestData( options );
+ var actual = StringExtensions.GetCommentLines(testData.Input, options).ToImmutableArray();
+ VerifyLineData( testData.Expected, actual );
+ }
+
+ [TestMethod]
+ public void EscapeComment_handles_newline_escapes( )
+ {
+ const string input = "line0\\nline1\\nline2";
+ string expected = "line0" + Environment.NewLine + "line1" + Environment.NewLine + "line2";
+ string actual = StringExtensions.EscapeComment(input);
+ Assert.AreEqual(expected, actual);
+ }
+
+ [TestMethod]
+ public void SplitLines_default_option_is_none( )
+ {
+ var testData = GetSplitLinesTestData(StringSplitOptions2.None);
+ var actual = StringExtensions.SplitLines(testData.Input).ToImmutableArray();
+ VerifyLineData(testData.Expected, actual);
+ }
+
+ [TestMethod]
+ [DataRow( StringSplitOptions2.None )]
+ [DataRow( StringSplitOptions2.RemoveEmptyEntries )]
+ [DataRow( StringSplitOptions2.TrimEntries )]
+ [DataRow( StringSplitOptions2.TrimEntries | StringSplitOptions2.RemoveEmptyEntries )]
+ public void SplitLines_with_options( StringSplitOptions2 options )
+ {
+ var testData = GetSplitLinesTestData( options );
+ var actual = StringExtensions.SplitLines(testData.Input, options).ToImmutableArray();
+ VerifyLineData( testData.Expected, actual );
+ }
+
+ [TestMethod]
+ public void MakeXmlSafe_handles_XML_Conversion( )
+ {
+ Assert.AreEqual("&", StringExtensions.MakeXmlSafe("&"));
+ Assert.AreEqual( "<text2>", StringExtensions.MakeXmlSafe(""));
+
+ // additional escapes are testable but it comes down to
+ // testing XText(...).ToString() at that point...
+ }
+
+ [TestMethod]
+ public void SkipDuplicates_skips_any_duplicate_line( )
+ {
+ ImmutableArray input = [
+ "text",
+ "text2",
+ "text2", // duplicate of previous line - expect removed,
+ "text", // duplicate of first line, but not previous - expect remains
+ ];
+
+ ImmutableArray expected = [
+ "text",
+ "text2",
+ /* "text2", // duplicate of previous line - expect removed, */
+ "text", // duplicate of first line, but not previous - expect remains
+ ];
+
+ var actual = StringExtensions.SkipDuplicates(input).ToImmutableArray();
+ VerifyLineData(expected, actual);
+ }
+
+ [TestMethod]
+ public void EscapeForComment_escapes_each_string( )
+ {
+ ImmutableArray input = [
+ "text\\npart2",
+ "text2",
+ "text2",
+ "text\\n",
+ "\\ntext"
+ ];
+
+ ImmutableArray expected = [
+ "text" + Environment.NewLine + "part2",
+ "text2",
+ "text2",
+ "text" + Environment.NewLine,
+ Environment.NewLine + "text",
+ ];
+
+ var actual = StringExtensions.EscapeForComment(input).ToImmutableArray();
+ VerifyLineData( expected, actual );
+ }
+
+ [TestMethod]
+ public void EscapeForXML_escapes_each_string( )
+ {
+ ImmutableArray input = [
+ "&this or that",
+ "",
+ "this or &that",
+ ];
+
+ ImmutableArray expected = [
+ "&this or that",
+ "<text2>",
+ "this or &that",
+ ];
+
+ var actual = StringExtensions.EscapeForXML(input).ToImmutableArray();
+ VerifyLineData( expected, actual );
+ }
+
+ private void VerifyLineData( ImmutableArray expected, ImmutableArray actual )
+ {
+ TestContext.Report( "expected", expected);
+ TestContext.Report( "actual", actual);
+ Assert.HasCount( expected.Length, actual );
+ for(int i = 0; i < expected.Length; ++i)
+ {
+ Assert.AreEqual( expected[ i ], actual[ i ], $"Mismatch on element {i}" );
+ }
+ }
+
+ private static LineData GetCommentLinesTestData( StringSplitOptions2 options )
+ {
+ // determine expected results based on options
+ string line0 = "Comment line0";
+ string line1 = " s e ";
+ string line2 = " ";
+ string line3 = string.Empty;
+ string line4 = "Comment line4";
+
+ string testInput = string.Join(Environment.NewLine, line0, line1, line2, line3, line4);
+
+ var bldr = ImmutableArray.CreateBuilder();
+
+ bldr.Add(line0); // never altered, always included.
+
+ // if Trim entries is specified, then trim lines 1 & 2
+ // These are the only input lines that can be trimmed.
+ if(options.HasFlag( StringSplitOptions2.TrimEntries ))
+ {
+ line1 = line1.Trim();
+ line2 = line2.Trim();
+ }
+
+ // never empty, but might be trimmed
+ bldr.Add(line1);
+
+ // line 2 & 3 might be empty strings and might be removed.
+ if(options.HasFlag( StringSplitOptions2.RemoveEmptyEntries ))
+ {
+ if(!string.IsNullOrEmpty( line2 ))
+ {
+ bldr.Add( line2 );
+ }
+
+ // IFF line3 is not empty AND not the same as previous line add it
+ // duplicates always removed.
+ if(!string.IsNullOrEmpty( line3 ) && (bldr[ ^1 ] != line3))
+ {
+ bldr.Add( line3 );
+ }
+ }
+ else
+ {
+ bldr.Add( line2 );
+
+ // IFF line3 is not the same as previous line add it; duplicates always removed.
+ if(bldr[ ^1 ] != line3)
+ {
+ bldr.Add( line3 );
+ }
+ }
+
+ bldr.Add( line4 );
+
+ return new(testInput, bldr.ToImmutable());
+ }
+
+ private static LineData GetSplitLinesTestData( StringSplitOptions2 options )
+ {
+ // determine expected results based on options
+ string line0 = "Comment line0";
+ string line1 = " s e ";
+ string line2 = " ";
+ string line3 = string.Empty;
+ string line4 = "Comment line4";
+
+ string testInput = string.Join(Environment.NewLine, line0, line1, line2, line3, line4);
+
+ var bldr = ImmutableArray.CreateBuilder();
+
+ bldr.Add( line0 ); // never altered, always included.
+
+ // if Trim entries is specified, then trim lines 1 & 2
+ // These are the only input lines that can be trimmed.
+ if(options.HasFlag( StringSplitOptions2.TrimEntries ))
+ {
+ line1 = line1.Trim();
+ line2 = line2.Trim();
+ }
+
+ // never empty, but might be trimmed
+ bldr.Add( line1 );
+
+ // line 2 & 3 might be empty strings and might be removed.
+ if(options.HasFlag( StringSplitOptions2.RemoveEmptyEntries ))
+ {
+ if(!string.IsNullOrEmpty( line2 ))
+ {
+ bldr.Add( line2 );
+ }
+
+ if(!string.IsNullOrEmpty( line3 ))
+ {
+ bldr.Add( line3 );
+ }
+ }
+ else
+ {
+ bldr.Add( line2 );
+ bldr.Add( line3 );
+ }
+
+ bldr.Add( line4 );
+
+ return new( testInput, bldr.ToImmutable() );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/TestContextExtensions.cs b/src/Ubiquity.NET.SrcGeneration.UT/TestContextExtensions.cs
new file mode 100644
index 000000000..45a1e913f
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/TestContextExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+
+namespace Ubiquity.NET.SrcGeneration.UT
+{
+ [ExcludeFromCodeCoverage]
+ internal static class TestContextExtensions
+ {
+ internal static void Report( this TestContext ctx, string title, ImmutableArray arrayVal )
+ {
+ ctx.WriteLine( "{0}[{1}]", title, arrayVal.Length );
+ for(int i = 0; i < arrayVal.Length; ++i)
+ {
+ ctx.WriteLine(" [{0}] = {1}", i, arrayVal[i]);
+ }
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SrcGeneration.UT/Ubiquity.NET.SrcGeneration.UT.csproj b/src/Ubiquity.NET.SrcGeneration.UT/Ubiquity.NET.SrcGeneration.UT.csproj
new file mode 100644
index 000000000..e31676d7d
--- /dev/null
+++ b/src/Ubiquity.NET.SrcGeneration.UT/Ubiquity.NET.SrcGeneration.UT.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0;net481
+ 12
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/src/Ubiquity.NET.SrcGeneration/CSharp/CSharpLanguage.cs b/src/Ubiquity.NET.SrcGeneration/CSharp/CSharpLanguage.cs
index f4cf3ab0d..d6116c7fb 100644
--- a/src/Ubiquity.NET.SrcGeneration/CSharp/CSharpLanguage.cs
+++ b/src/Ubiquity.NET.SrcGeneration/CSharp/CSharpLanguage.cs
@@ -12,7 +12,11 @@ public static class CSharpLanguage
/// Closing of a scope for C#
public const string ScopeClose = "}";
- /// Gets the language keywords used to make identifiers
+ /// Gets the language keywords
+ ///
+ /// This is normally used from within
+ /// to escape keywords as identifiers. But is available for any use.
+ ///
public static ImmutableArray KeyWords { get; }
= [ // Source: Language spec. §6.4.4 Keywords
"abstract",
@@ -97,13 +101,24 @@ public static class CSharpLanguage
/// Makes an identifier (Escaping a language keyword)
/// identifier string to convert
/// Syntactically valid identifier
+ ///
+ /// Current implementation simplisticly performs keyword escaping AND
+ /// conversion of space to `'_'`. Specifically, it does NOT (yet anyway)
+ /// validate that the result satisfies the language definition of an
+ /// identifier (which limits the characters allowed and further restricts
+ /// the first such character)
+ ///
public static string MakeIdentifier( this string self )
{
ArgumentNullException.ThrowIfNull( self );
// always replace invalid characters
// TODO: more sophisticated Regex that matches anything NOT a valid identifier char
+#if NETSTANDARD2_0
+ string retVal = self.Replace( " ", "_" );
+#else
string retVal = self.Replace( " ", "_", StringComparison.Ordinal );
+#endif
return KeyWords.Contains( self )
? $"@{retVal}"
: retVal;
diff --git a/src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterCsExtensions.cs b/src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterExtensions.cs
similarity index 85%
rename from src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterCsExtensions.cs
rename to src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterExtensions.cs
index f5868cc67..122566395 100644
--- a/src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterCsExtensions.cs
+++ b/src/Ubiquity.NET.SrcGeneration/CSharp/IndentedTextWriterExtensions.cs
@@ -3,10 +3,10 @@
namespace Ubiquity.NET.SrcGeneration.CSharp
{
- /// Extensions to to support C# source generation
+ /// Utility extensions for an specific to the C# language
[SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "extension" )]
[SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "extension" )]
- public static class IndentedTextWriterCsExtensions
+ public static class IndentedTextWriterExtensions
{
/// Writes a standard auto-generated comment block
/// Writer to apply extension method to
@@ -21,12 +21,10 @@ public static void WriteAutoGeneratedComment( this IndentedTextWriter self, stri
if(!string.IsNullOrWhiteSpace( source ))
{
- self.WriteLine( $"// From: {source}" );
- }
- else
- {
- self.WriteLine( "//" );
+ self.WriteLine( $"// From: {source}" );
}
+
+ self.WriteLine( "//" );
}
/// Writes a standard auto-generated comment block returning the scope for additional contents
@@ -37,11 +35,13 @@ public static void WriteAutoGeneratedComment( this IndentedTextWriter self, stri
///
/// To remain syntactically valid all lines written to the writer within this block
/// MUST include a leading "//" (this does NOT alter the prefix requirement
- /// for the writer).
+ /// for the writer). The resulting scope is NOT indented.
///
public static IDisposable WriteAutoGeneratedCommentBlock( this IndentedTextWriter self, string toolName, string toolVersion )
{
ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace( toolName );
+ ArgumentException.ThrowIfNullOrWhiteSpace( toolVersion );
string open = $"""
// ------------------------------------------------------------------------------
@@ -65,6 +65,9 @@ public static IDisposable WriteAutoGeneratedCommentBlock( this IndentedTextWrite
/// Disposable scope for the namespace (closes the scope on dispose)
public static IDisposable Namespace( this IndentedTextWriter self, string namespaceName )
{
+ ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace(namespaceName);
+
return self.Scope( $"namespace {namespaceName}" );
}
@@ -73,8 +76,11 @@ public static IDisposable Namespace( this IndentedTextWriter self, string namesp
/// Access (prefix) for the struct declaration
/// name of the structure
/// Disposable scope that closes the struct declaration on
- public static IDisposable Struct( this IndentedTextWriter self, string access, string structName )
+ public static IDisposable Struct( this IndentedTextWriter self, string? access, string structName )
{
+ ArgumentException.ThrowIfNullOrWhiteSpace( structName );
+
+ access ??= string.Empty;
return self.Scope( $"{access} struct {structName}" );
}
@@ -104,15 +110,17 @@ public static IDisposable Scope( this IndentedTextWriter self, string? leadingLi
/// Writes as a multi-line comment
/// Writer to apply extension method to
/// Text of the comment
+ /// If is , empty, or all whitespace then this is a NOP
public static void MultiLineComment( this IndentedTextWriter self, string? txt )
{
ArgumentNullException.ThrowIfNull( self );
- if(txt is null)
+
+ if(string.IsNullOrWhiteSpace(txt))
{
return;
}
- string[] lines = [ .. txt.GetCommentLines() ];
+ string[] lines = [ .. txt!.GetCommentLines() ];
if(lines.Length > 0)
{
using(self.Block( "/*", "*/" ))
@@ -130,12 +138,12 @@ public static void MultiLineComment( this IndentedTextWriter self, string? txt )
/// access modifier (prefix) for the declaration
/// Name of the class to generate
/// Disposable scope that closes the class declaration on
- public static IDisposable Class( this IndentedTextWriter self, string access, string className )
+ public static IDisposable Class( this IndentedTextWriter self, string? access, string className )
{
- ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace( className );
- self.WriteLine( $"{access} class {className}" );
- return self.Scope();
+ access ??= string.Empty;
+ return self.Scope( $"{access} class {className}" );
}
}
}
diff --git a/src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterCsExtension.cs b/src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterExtensions.cs
similarity index 86%
rename from src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterCsExtension.cs
rename to src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterExtensions.cs
index 21d44e5a1..1cb1f14f2 100644
--- a/src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterCsExtension.cs
+++ b/src/Ubiquity.NET.SrcGeneration/CSharp/TextWriterExtensions.cs
@@ -6,7 +6,7 @@ namespace Ubiquity.NET.SrcGeneration.CSharp
/// Utility extensions for a specific to the C# language
[SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "extension" )]
[SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "extension" )]
- public static class TextWriterCsExtension
+ public static class TextWriterExtensions
{
/// Writes an attribute as a line
/// The writer to write to
@@ -15,6 +15,7 @@ public static class TextWriterCsExtension
public static void WriteAttributeLine( this TextWriter self, string attributeName, params string[] attribArgs )
{
ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace(attributeName);
self.WriteAttribute( attributeName, attribArgs );
self.WriteLine();
@@ -27,11 +28,12 @@ public static void WriteAttributeLine( this TextWriter self, string attributeNam
public static void WriteAttribute(this TextWriter self, string attributeName, params string[] attribArgs )
{
ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace( attributeName );
self.Write( $"[{attributeName}" );
if(attribArgs.Length > 0)
{
- self.Write( $"({string.Join( ",", attribArgs )})" );
+ self.Write( $"({string.Join( ", ", attribArgs )})" );
}
self.Write( "]" );
@@ -39,7 +41,7 @@ public static void WriteAttribute(this TextWriter self, string attributeName, pa
/// Writes an XML Doc comment summary
/// The writer to write to
- /// Text to include in the summary (Nothing is written if this is
+ /// Text to include in the summary (Nothing is written if this is or all whitespace
public static void WriteSummaryComment(this TextWriter self, string? description )
{
ArgumentNullException.ThrowIfNull( self );
@@ -60,12 +62,13 @@ public static void WriteSummaryComment(this TextWriter self, string? description
public static void WriteRemarksComment( this TextWriter self, string? txt )
{
ArgumentNullException.ThrowIfNull( self );
+
if(string.IsNullOrWhiteSpace( txt ))
{
return;
}
- string[] lines = [ .. txt.GetCommentLines() ];
+ string[] lines = [ .. txt!.GetCommentLines() ];
if(lines.Length > 0)
{
self.WriteLine( "/// " );
@@ -88,7 +91,7 @@ public static void WriteRemarksComment( this TextWriter self, string? txt )
/// is used as the summary. If is also empty or all Whitespace then nothing
/// is output.
///
- public static void WriteSummaryAndRemarksComments( this TextWriter self, string? txt, string defaultSummary = "" )
+ public static void WriteSummaryAndRemarksComments( this TextWriter self, string? txt, string? defaultSummary = null )
{
ArgumentNullException.ThrowIfNull( self );
@@ -96,14 +99,15 @@ public static void WriteSummaryAndRemarksComments( this TextWriter self, string?
{
if(!string.IsNullOrWhiteSpace( defaultSummary ))
{
- self.WriteLine( $"/// {defaultSummary}" );
+ self.WriteLine( $"/// {defaultSummary!.Trim()}" );
}
return;
}
- self.WriteLine( $"/// {defaultSummary}" );
- string[] lines = [ .. txt.GetCommentLines() ];
+ ArgumentException.ThrowIfNullOrWhiteSpace(defaultSummary);
+ self.WriteLine( $"/// {defaultSummary.Trim()}" );
+ string[] lines = [ .. txt!.GetCommentLines() ];
if(lines.Length > 0)
{
// summary + remarks.
@@ -123,6 +127,9 @@ public static void WriteSummaryAndRemarksComments( this TextWriter self, string?
/// Namespace for the using directive
public static void WriteUsingDirective(this TextWriter self, string namespaceName )
{
+ ArgumentNullException.ThrowIfNull( self );
+ ArgumentException.ThrowIfNullOrWhiteSpace(namespaceName);
+
self.WriteLine( $"using {namespaceName};" );
}
}
diff --git a/src/Ubiquity.NET.SrcGeneration/IndentedTextWriterExtensions.cs b/src/Ubiquity.NET.SrcGeneration/IndentedTextWriterExtensions.cs
index 4968460f5..c2aea91b0 100644
--- a/src/Ubiquity.NET.SrcGeneration/IndentedTextWriterExtensions.cs
+++ b/src/Ubiquity.NET.SrcGeneration/IndentedTextWriterExtensions.cs
@@ -3,27 +3,29 @@
namespace Ubiquity.NET.SrcGeneration
{
- /// Extensions to to support C# source generation
+ /// Extensions to to support generic source generation
[SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "extension" )]
[SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "extension" )]
public static class IndentedTextWriterExtensions
{
- // NOTE: use of `extension` here requires newer version of Roslyn due to bug in preview implementation
- // See:
- // https://github.com/dotnet/roslyn/issues/78135
- // https://github.com/dotnet/roslyn/issues/78042
- // Fixed in mainline but not available as part of release VS 2019
-
/// Writes an indented block to an
/// Writer to apply extension method to
/// Opening value of block (on it's own line)
/// Closing value of block (on it's own line)
/// Line of text preceding the block
/// Indicates if additional content written is indented or not [Default: ]
- /// that will automatically emit the closing line and out dent the writer.
+ /// that will automatically emit and out dent the writer.
+ ///
+ ///
+ /// This does NOT end the line. This allows writing a comment or other output after the block is closed on the same line
+ /// as the .
+ ///
+ ///
public static IDisposable Block(this IndentedTextWriter self, string open, string close, string? leadingLine = null, bool indented = true )
{
ArgumentNullException.ThrowIfNull( self );
+ ArgumentNullException.ThrowIfNull( open );
+ ArgumentNullException.ThrowIfNull( close );
if(leadingLine is not null)
{
@@ -39,14 +41,14 @@ public static IDisposable Block(this IndentedTextWriter self, string open, strin
// presence of this when using `extension` keyword triggers bugs in C#14 preview 3
// https://github.com/dotnet/roslyn/issues/78135
// https://github.com/dotnet/roslyn/issues/78042
- return new DisposableAction( ( ) =>
+ return new Extensions.DisposableAction( ( ) =>
{
if(indented)
{
--self.Indent;
}
- self.WriteLine( close );
+ self.Write( close );
} );
}
@@ -55,12 +57,14 @@ public static IDisposable Block(this IndentedTextWriter self, string open, strin
/// Disposable that when invoked, will reduce the indentation.
public static IDisposable PushIndent( this IndentedTextWriter self )
{
+ ArgumentNullException.ThrowIfNull( self );
+
++self.Indent;
// presence of lambda when using `extension` keyword triggers bugs in C#14 preview 3
// https://github.com/dotnet/roslyn/issues/78135
// https://github.com/dotnet/roslyn/issues/78042
- return new DisposableAction( ( ) => --self.Indent );
+ return new Extensions.DisposableAction( ( ) => --self.Indent );
}
/// Writes a blank line (New line, with no additional characters or whitespace)
diff --git a/src/Ubiquity.NET.SrcGeneration/StringExtensions.cs b/src/Ubiquity.NET.SrcGeneration/StringExtensions.cs
index ce20dde25..162d4b3e4 100644
--- a/src/Ubiquity.NET.SrcGeneration/StringExtensions.cs
+++ b/src/Ubiquity.NET.SrcGeneration/StringExtensions.cs
@@ -3,41 +3,32 @@
namespace Ubiquity.NET.SrcGeneration
{
- // TODO: What version of the language?
-
- /// Target language for creating identifier symbols
- public enum TargetLang
- {
- /// Use formatting for C# language
- CSharp = 0,
-
- /// Use formatting for VB.Net language
- VisualBasic = 1,
-
- /// Use formatting for F# language
- FSharp = 2,
- }
-
/// Utility class to host extensions for a .
- [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "extension" )]
- [SuppressMessage( "Naming", "CA1708:Identifiers should differ by more than case", Justification = "extension" )]
- [SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "extension" )]
public static class StringExtensions
{
- /// Gets the lines of this instance as a comment
+ /// Gets the lines of this instance as a contents for a comment
/// String to apply extension to
/// String split options to use when splitting lines
- /// Enumerable sequence of comment strings without any comment prefix (That is, no language specific leading'//', '/*' etc...)
- public static IEnumerable GetCommentLines( this string self, StringSplitOptions options = StringSplitOptions.TrimEntries )
+ /// Enumerable sequence of comment strings without any language specific leading or trailing delimiters
+ ///
+ /// This is generally used by language specific extensions that will also emit the comment leading/trailiing text as needed.
+ /// Thus it is a general language neutral facility that is used to produce the final language specific comments.
+ /// It will perform the following on the input string:
+ /// 1) to ensure character escaping is applied to the whole string
+ /// 2) to split the string into distinct lines
+ /// 3) to ensure the lines are valid for XML doc comments
+ /// 4) to fold duplicate entries into one (usually to reduce multiple blank lines to one)
+ ///
+ public static IEnumerable GetCommentLines( this string self, StringSplitOptions2 options = StringSplitOptions2.TrimEntries )
{
ArgumentNullException.ThrowIfNull( self );
// For now, naive conversion - just splits on newlines
// more sophisticated implementation could split on word boundaries based on length...
return self.EscapeComment()
- .SplitLines( options )
- .EscapeForXML()
- .SkipDuplicates();
+ .SplitLines( options )
+ .EscapeForXML()
+ .SkipDuplicates();
}
/// Escapes characters in a comment
@@ -52,21 +43,46 @@ public static string EscapeComment( this string self )
ArgumentNullException.ThrowIfNull( self );
// For now, the only escape is a newline '\n'
+#if NETSTANDARD2_0
+ return self.Replace( "\\n", Environment.NewLine );
+#else
return self.Replace( "\\n", Environment.NewLine, StringComparison.Ordinal );
+#endif
}
/// Splits a string into a sequence of lines
/// String to apply extension to
/// String split options to use when splitting lines
/// Enumerable sequence of strings, one for each line in the original
- public static string[] SplitLines( this string self, StringSplitOptions splitOptions = StringSplitOptions.None )
+ ///
+ ///
+ /// For runtimes prior to .NET 5 the behavior of
+ /// is emulated here. The implementation of that emulation is not as performant as the
+ /// official form in later runtimes. This emulation was chosen for correctness of behavior
+ /// and simplicity of implementation over performance. If absolute best performance is
+ /// desired then use the latest runtime.
+ ///
+ ///
+ public static IEnumerable SplitLines( this string self, StringSplitOptions2 splitOptions = StringSplitOptions2.None )
{
ArgumentNullException.ThrowIfNull( self );
- return self.Split( MixedLineEndings, splitOptions );
+
+#if !NET5_0_OR_GREATER
+ // StringSplitOptions.TrimeEntries member is not available, do it the hard/slow way
+ if(splitOptions.HasFlag(StringSplitOptions2.TrimEntries))
+ {
+ var options = (StringSplitOptions)((int)splitOptions & ~(int)StringSplitOptions2.TrimEntries);
+ return from s in self.Split( MixedLineEndings, options)
+ let t = s.Trim()
+ where !splitOptions.HasFlag(StringSplitOptions2.RemoveEmptyEntries) || !string.IsNullOrEmpty(t)
+ select t;
+ }
+#endif
+ return self.Split( MixedLineEndings, (StringSplitOptions)splitOptions);
}
// TODO: WithLines(Action> op)
- // finds all line endings and provides each as a span to op
+ // finds all line endings and provides each line as a span to op
// This avoids the problem of IEnumerable> lifetime management
// though that is plausible with a custom implementation instead of a generated
// iterator...
@@ -74,9 +90,11 @@ public static string[] SplitLines( this string self, StringSplitOptions splitOpt
/// Escapes a string for use in XML
/// String to apply extension to
/// XML safe escaped string
+ /// This will perform escaping of characters for XML such as conversion of `&` into `&` etc...
public static string MakeXmlSafe( this string self )
{
ArgumentNullException.ThrowIfNull( self );
+
return new XText( self ).ToString();
}
@@ -107,31 +125,39 @@ public static IEnumerable EscapeForComment( this IEnumerable sel
/// Sequence to apply this extension to
/// Sequence of strings that has no duplicate side by side entries
///
- /// For the purposes of this method `duplicate` means "identical to previous". Thus,
- /// it is possible for multiple entries to have the same value in the result, just none
- /// where the previous entry is the same as the current one. This is normally used to
- /// when importing from a format that contains multiple new lines in a row so they are
- /// converted to a single new line.
+ /// For the purposes of this method `duplicate` means "identical to previous". Thus,
+ /// it is possible for multiple entries in to have the same value
+ /// in the result, just none where the previous entry is the same as the current one. This
+ /// is normally used when importing from a format that contains multiple new lines in a
+ /// row so they are converted to a single new line.
+ /// The returned value is a deferred iterator, the actual work of duplicate detection
+ /// and removal is deferred until needed by the caller and stopped whenever the caller ceases
+ /// to enumerate more items.
///
public static IEnumerable SkipDuplicates( this IEnumerable self )
{
+ ArgumentNullException.ThrowIfNull( self );
+
string? oldVal = null;
- foreach(string val in self)
- {
- if(val != oldVal)
+ return self.Where((s)=>
{
- yield return val;
- }
-
- oldVal = val;
- }
+ bool retVal = s != oldVal;
+ oldVal = s;
+ return retVal;
+ } );
}
+#pragma warning disable IDE0002
+// names can't be simplified further due to weird ambiguities with how extensions are resolved
+// net stadard 2.0 does NOT contain the static methods for argument validation on exceptions.
+// Thus in tat runtime they are polly fill extensions, but they have the same name as instance
+// extensions - those win out and collide causing mass confusion.
private static readonly string [] MixedLineEndings =
[
- LineEndingKind.CarriageReturnLineFeed.LineEnding(),
- LineEndingKind.CarriageReturn.LineEnding(),
- LineEndingKind.LineFeed.LineEnding(),
+ Extensions.StringNormalizer.LineEnding(Extensions.LineEndingKind.CarriageReturnLineFeed),
+ Extensions.StringNormalizer.LineEnding(Extensions.LineEndingKind.CarriageReturn),
+ Extensions.StringNormalizer.LineEnding(Extensions.LineEndingKind.LineFeed),
];
+#pragma warning restore IDE0002
}
}
diff --git a/src/Ubiquity.NET.SrcGeneration/Ubiquity.NET.SrcGeneration.csproj b/src/Ubiquity.NET.SrcGeneration/Ubiquity.NET.SrcGeneration.csproj
index 1f21e7652..4f41ecf3a 100644
--- a/src/Ubiquity.NET.SrcGeneration/Ubiquity.NET.SrcGeneration.csproj
+++ b/src/Ubiquity.NET.SrcGeneration/Ubiquity.NET.SrcGeneration.csproj
@@ -1,40 +1,40 @@
-
- net8.0
- AnyCPU
-
- preview
- True
- True
+
+ net8.0;netstandard2.0
+ AnyCPU
+
+ preview
+ True
+ True
-
- true
- 4.9.0
- .NET Foundation,Ubiquity.NET
- false
- General use Support for Source Generators
- Extensions,.NET,Ubiquity.NET
- ReadMe.md
- https://github.com/UbiquityDotNET/Llvm.NET
- https://github.com/UbiquityDotNET/Llvm.NET.git
- git
- Apache-2.0 WITH LLVM-exception
- true
- snupkg
-
+
+ true
+ 4.9.0
+ .NET Foundation,Ubiquity.NET
+ false
+ General use Support for Source Generators
+ Extensions,.NET,Ubiquity.NET
+ ReadMe.md
+ https://github.com/UbiquityDotNET/Llvm.NET
+ https://github.com/UbiquityDotNET/Llvm.NET.git
+ git
+ Apache-2.0 WITH LLVM-exception
+ true
+ snupkg
+
-
-
-
+
+
+
-
-
-
- all
- false
- Analyzer
-
-
+
+
+
+ all
+ false
+ Analyzer
+
+