From 4a9c53b798bbe7f4eec6f2cb6cc4cdc6654aacff Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:38:31 -0700 Subject: [PATCH 01/19] Add solution-wide annotation references as non-transitive assets --- Directory.build.props | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Directory.build.props diff --git a/Directory.build.props b/Directory.build.props new file mode 100644 index 000000000..5a4ac0cf9 --- /dev/null +++ b/Directory.build.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From fe5d51d71c7cb206296885a9f4df283613cff61d Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:39:01 -0700 Subject: [PATCH 02/19] Standard MIT license text --- Scripts/COPYRIGHT | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Scripts/COPYRIGHT diff --git a/Scripts/COPYRIGHT b/Scripts/COPYRIGHT new file mode 100644 index 000000000..1b46d33f0 --- /dev/null +++ b/Scripts/COPYRIGHT @@ -0,0 +1,16 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The MIT License (MIT) +// Copyright © 2024 Brandon Thetford (@dodexahedron) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, +// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// From 9682b4d6e94144c78a34aaeaee92254cbb38c599 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 23:15:11 -0700 Subject: [PATCH 03/19] Add PowerShell module for convenient analyzer rebuilds --- Scripts/Terminal.Gui.PowerShell.psm1 | 100 +++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 Scripts/Terminal.Gui.PowerShell.psm1 diff --git a/Scripts/Terminal.Gui.PowerShell.psm1 b/Scripts/Terminal.Gui.PowerShell.psm1 new file mode 100644 index 000000000..6e619ba20 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.psm1 @@ -0,0 +1,100 @@ +Function Build-Analyzers { + [CmdletBinding()] + param( + [Parameter(Mandatory=$false, HelpMessage="Automatically close running Visual Studio processes which have the Terminal.sln solution loaded, before taking any other actions.")] + [switch]$AutoClose, + [Parameter(Mandatory=$false, HelpMessage="Automatically start a new Visual Studio process and load the solution after completion.")] + [switch]$AutoLaunch, + [Parameter(Mandatory=$false, HelpMessage="Carry out operations unconditionally and do not prompt for confirmation.")] + [switch]$Force, + [Parameter(Mandatory=$false, HelpMessage="Do not delete the bin and obj folders before building the analyzers.")] + [switch]$NoClean, + [Parameter(Mandatory=$false, HelpMessage="Write less text output to the terminal.")] + [switch]$Quiet + ) + + if($AutoClose) { + if(!$Quiet) { + Write-Host Closing Visual Studio processes + } + Close-Solution + } + + if($Force){ + $response = 'Y' + } + elseif(!$Force && $NoClean){ + $response = ($r = Read-Host "Pre-build Terminal.Gui.InternalAnalyzers without removing old build artifacts? [Y/n]") ? $r : 'Y' + } + else{ + $response = ($r = Read-Host "Delete bin and obj folders for Terminal.Gui and Terminal.Gui.InternalAnalyzers and pre-build Terminal.Gui.InternalAnalyzers? [Y/n]") ? $r : 'Y' + } + + if (($response -ne 'Y')) { + Write-Host Took no action + return + } + + New-Variable -Name solutionRoot -Visibility Public -Value (Resolve-Path ..) + Push-Location $solutionRoot + New-Variable -Name solutionFile -Visibility Public -Value (Resolve-Path ./Terminal.sln) + $mainProjectRoot = Resolve-Path ./Terminal.Gui + $mainProjectFile = Join-Path $mainProjectRoot Terminal.Gui.csproj + $analyzersRoot = Resolve-Path ./Analyzers + $internalAnalyzersProjectRoot = Join-Path $analyzersRoot Terminal.Gui.Analyzers.Internal + $internalAnalyzersProjectFile = Join-Path $internalAnalyzersProjectRoot Terminal.Gui.Analyzers.Internal.csproj + + if(!$NoClean) { + if(!$Quiet) { + Write-Host Deleting bin and obj folders for Terminal.Gui + } + if(Test-Path $mainProjectRoot/bin) { + Remove-Item -Recurse -Force $mainProjectRoot/bin + Remove-Item -Recurse -Force $mainProjectRoot/obj + } + + if(!$Quiet) { + Write-Host Deleting bin and obj folders for Terminal.Gui.InternalAnalyzers + } + if(Test-Path $internalAnalyzersProjectRoot/bin) { + Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/bin + Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/obj + } + } + + if(!$Quiet) { + Write-Host Building analyzers in Debug configuration + } + dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Debug + + if(!$Quiet) { + Write-Host Building analyzers in Release configuration + } + dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Release + + if(!$AutoLaunch) { + Write-Host -ForegroundColor Green Finished. Restart Visual Studio for changes to take effect. + } else { + if(!$Quiet) { + Write-Host -ForegroundColor Green Finished. Re-loading Terminal.sln. + } + Open-Solution + } + + return +} + +Function Open-Solution { + Invoke-Item $solutionFile + return +} + +Function Close-Solution { + $vsProcesses = Get-Process -Name devenv | Where-Object { ($_.CommandLine -Match ".*Terminal\.sln.*" -or $_.MainWindowTitle -Match "Terminal.*") } + Stop-Process -InputObject $vsProcesses + Remove-Variable vsProcesses +} + +Export-ModuleMember -Function Build-Analyzers +Export-ModuleMember -Function Open-Solution +Export-ModuleMember -Function Close-Solution From e0994ec1e62843fc3a70cd022183f9a4fdf24d38 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:41:22 -0700 Subject: [PATCH 04/19] Add analyzer projects - Analyzer project - Test project for analyzers - Console application for Roslyn analyzer debugging --- ...al.Gui.Analyzers.Internal.Debugging.csproj | 26 +++++ ...rminal.Gui.Analyzers.Internal.Tests.csproj | 50 ++++++++++ ...nalyzers.Internal.Tests.csproj.DotSettings | 3 + .../Terminal.Gui.Analyzers.Internal.csproj | 98 +++++++++++++++++++ ....Gui.Analyzers.Internal.csproj.DotSettings | 4 + 5 files changed, 181 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Terminal.Gui.Analyzers.Internal.Debugging.csproj create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj.DotSettings create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj.DotSettings diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Terminal.Gui.Analyzers.Internal.Debugging.csproj b/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Terminal.Gui.Analyzers.Internal.Debugging.csproj new file mode 100644 index 000000000..03b5cf379 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Terminal.Gui.Analyzers.Internal.Debugging.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + all + Analyzer + true + + + + diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj new file mode 100644 index 000000000..4020bd357 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + enable + 12 + enable + false + true + true + True + portable + $(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL;CODE_ANALYSIS + enable + True + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + Analyzer + true + + + + + + + + + + + diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj.DotSettings b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj.DotSettings new file mode 100644 index 000000000..cd5ef68b8 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Terminal.Gui.Analyzers.Internal.Tests.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj b/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj new file mode 100644 index 000000000..1b7ecaf9b --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj @@ -0,0 +1,98 @@ + + + + netstandard2.0 + + + + Library + 12 + enable + Terminal.Gui.Analyzers.Internal + disable + true + true + True + latest-recommended + 7 + UTF-8 + true + true + true + true + true + $(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL;CODE_ANALYSIS + True + true + true + + + + + + + + + + + + + + + $(NoWarn);nullable;CA1067 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj.DotSettings b/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj.DotSettings new file mode 100644 index 000000000..6c2c0e27d --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Terminal.Gui.Analyzers.Internal.csproj.DotSettings @@ -0,0 +1,4 @@ + + CSharp120 + InternalsOnly + False \ No newline at end of file From 508331a2e60c7827026ea1454f94d41338bc2724 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:42:09 -0700 Subject: [PATCH 05/19] Common string constants used by various components --- .../Constants/Strings.cs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Constants/Strings.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Constants/Strings.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Constants/Strings.cs new file mode 100644 index 000000000..e03d5fcc3 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Constants/Strings.cs @@ -0,0 +1,202 @@ +// ReSharper disable MemberCanBePrivate.Global + +using System; +using System.CodeDom.Compiler; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Constants; + +/// String constants for frequently-used boilerplate. +/// These are for performance, instead of using Roslyn to build it all during execution of analyzers. +internal static class Strings +{ + internal const string AnalyzersAttributesNamespace = $"{InternalAnalyzersNamespace}.Attributes"; + + internal const string AssemblyExtendedEnumTypeAttributeFullName = $"{AnalyzersAttributesNamespace}.{nameof (AssemblyExtendedEnumTypeAttribute)}"; + + internal const string DefaultTypeNameSuffix = "Extensions"; + + internal const string FallbackClassNamespace = $"{TerminalGuiRootNamespace}"; + + internal const string InternalAnalyzersNamespace = $"{AnalyzersRootNamespace}.Internal"; + + internal const string TerminalGuiRootNamespace = "Terminal.Gui"; + + private const string AnalyzersRootNamespace = $"{TerminalGuiRootNamespace}.Analyzers"; + private const string NetStandard20CompatibilityNamespace = $"{InternalAnalyzersNamespace}.Compatibility"; + + /// + /// Names of dotnet namespaces and types. Included as compile-time constants to avoid unnecessary work for the Roslyn + /// source generators. + /// + /// Implemented as nested static types because XmlDoc doesn't work on namespaces. + internal static class DotnetNames + { + /// Fully-qualified attribute type names. Specific applications (uses) are in . + internal static class Attributes + { + /// + internal const string CompilerGenerated = $"{Namespaces.System_Runtime_CompilerServices}.{nameof (CompilerGeneratedAttribute)}"; + + /// + internal const string DebuggerNonUserCode = $"{Namespaces.System_Diagnostics}.{nameof (DebuggerNonUserCodeAttribute)}"; + + /// + internal const string ExcludeFromCodeCoverage = $"{Namespaces.System_Diagnostics_CodeAnalysis}.{nameof (ExcludeFromCodeCoverageAttribute)}"; + + internal const string Flags = $"{Namespaces.SystemNS}.{nameof (FlagsAttribute)}"; + + internal const string GeneratedCode = $"{Namespaces.System_CodeDom_Compiler}.{nameof (GeneratedCodeAttribute)}"; + + /// + /// Use of this attribute should be carefully evaluated. + internal const string MethodImpl = $"{Namespaces.System_Runtime_CompilerServices}.{nameof (MethodImplAttribute)}"; + + /// Attributes formatted for use in code, including square brackets. + internal static class Applications + { + // ReSharper disable MemberHidesStaticFromOuterClass + internal const string Flags = $"[{Attributes.Flags}]"; + + /// + internal const string GeneratedCode = $"""[{Attributes.GeneratedCode}("{InternalAnalyzersNamespace}","1.0")]"""; + + /// + /// Use of this attribute should be carefully evaluated. + internal const string AggressiveInlining = $"[{MethodImpl}({Types.MethodImplOptions}.{nameof (MethodImplOptions.AggressiveInlining)})]"; + + /// + internal const string DebuggerNonUserCode = $"[{Attributes.DebuggerNonUserCode}]"; + + /// + internal const string CompilerGenerated = $"[{Attributes.CompilerGenerated}]"; + + /// + internal const string ExcludeFromCodeCoverage = $"[{Attributes.ExcludeFromCodeCoverage}]"; + + // ReSharper restore MemberHidesStaticFromOuterClass + } + } + + /// Names of dotnet namespaces. + internal static class Namespaces + { + internal const string SystemNS = nameof (System); + internal const string System_CodeDom = $"{SystemNS}.{nameof (System.CodeDom)}"; + internal const string System_CodeDom_Compiler = $"{System_CodeDom}.{nameof (System.CodeDom.Compiler)}"; + internal const string System_ComponentModel = $"{SystemNS}.{nameof (System.ComponentModel)}"; + internal const string System_Diagnostics = $"{SystemNS}.{nameof (System.Diagnostics)}"; + internal const string System_Diagnostics_CodeAnalysis = $"{System_Diagnostics}.{nameof (System.Diagnostics.CodeAnalysis)}"; + internal const string System_Numerics = $"{SystemNS}.{nameof (System.Numerics)}"; + internal const string System_Runtime = $"{SystemNS}.{nameof (System.Runtime)}"; + internal const string System_Runtime_CompilerServices = $"{System_Runtime}.{nameof (System.Runtime.CompilerServices)}"; + } + + internal static class Types + { + internal const string Attribute = $"{Namespaces.SystemNS}.{nameof (System.Attribute)}"; + internal const string AttributeTargets = $"{Namespaces.SystemNS}.{nameof (System.AttributeTargets)}"; + internal const string AttributeUsageAttribute = $"{Namespaces.SystemNS}.{nameof (System.AttributeUsageAttribute)}"; + + internal const string MethodImplOptions = + $"{Namespaces.System_Runtime_CompilerServices}.{nameof (System.Runtime.CompilerServices.MethodImplOptions)}"; + } + } + + internal static class Templates + { + internal const string AutoGeneratedCommentBlock = $""" + //------------------------------------------------------------------------------ + // + // This file and the code it contains was generated by a source generator in + // the {InternalAnalyzersNamespace} library. + // + // Modifications to this file are not supported and will be lost when + // source generation is triggered, either implicitly or explicitly. + // + //------------------------------------------------------------------------------ + """; + + /// + /// A set of explicit type aliases to work around Terminal.Gui having name collisions with types like + /// . + /// + internal const string DotnetExplicitTypeAliasUsingDirectives = $""" + using Attribute = {DotnetNames.Types.Attribute}; + using AttributeUsageAttribute = {DotnetNames.Types.AttributeUsageAttribute}; + using GeneratedCode = {DotnetNames.Attributes.GeneratedCode}; + """; + + /// Using directives for common namespaces in generated code. + internal const string DotnetNamespaceUsingDirectives = $""" + using {DotnetNames.Namespaces.SystemNS}; + using {DotnetNames.Namespaces.System_CodeDom}; + using {DotnetNames.Namespaces.System_CodeDom_Compiler}; + using {DotnetNames.Namespaces.System_ComponentModel}; + using {DotnetNames.Namespaces.System_Numerics}; + using {DotnetNames.Namespaces.System_Runtime}; + using {DotnetNames.Namespaces.System_Runtime_CompilerServices}; + """; + + /// + /// A set of empty namespaces that MAY be referenced in generated code, especially in using statements, + /// which are always included to avoid additional complexity due to conditional compilation. + /// + internal const string DummyNamespaceDeclarations = $$""" + // These are dummy declarations to avoid complexity with conditional compilation. + #pragma warning disable IDE0079 // Remove unnecessary suppression + #pragma warning disable RCS1259 // Remove empty syntax + namespace {{TerminalGuiRootNamespace}} { } + namespace {{AnalyzersRootNamespace}} { } + namespace {{InternalAnalyzersNamespace}} { } + namespace {{NetStandard20CompatibilityNamespace}} { } + namespace {{AnalyzersAttributesNamespace}} { } + #pragma warning restore RCS1259 // Remove empty syntax + #pragma warning restore IDE0079 // Remove unnecessary suppression + """; + + internal const string StandardHeader = $""" + {AutoGeneratedCommentBlock} + // ReSharper disable RedundantUsingDirective + // ReSharper disable once RedundantNullableDirective + {NullableContextDirective} + + {StandardUsingDirectivesText} + """; + + /// + /// Standard set of using directives for generated extension method class files. + /// Not all are always needed, but all are included so we don't have to worry about it. + /// + internal const string StandardUsingDirectivesText = $""" + {DotnetNamespaceUsingDirectives} + {DotnetExplicitTypeAliasUsingDirectives} + using {TerminalGuiRootNamespace}; + using {AnalyzersRootNamespace}; + using {InternalAnalyzersNamespace}; + using {AnalyzersAttributesNamespace}; + using {NetStandard20CompatibilityNamespace}; + """; + + internal const string AttributesForGeneratedInterfaces = $""" + {DotnetNames.Attributes.Applications.GeneratedCode} + {DotnetNames.Attributes.Applications.CompilerGenerated} + """; + + internal const string AttributesForGeneratedTypes = $""" + {DotnetNames.Attributes.Applications.GeneratedCode} + {DotnetNames.Attributes.Applications.CompilerGenerated} + {DotnetNames.Attributes.Applications.DebuggerNonUserCode} + {DotnetNames.Attributes.Applications.ExcludeFromCodeCoverage} + """; + + /// + /// Preprocessor directive to enable nullability context for generated code.
+ /// This should always be emitted, as it applies only to generated code.
+ /// As such, generated code MUST be properly annotated. + ///
+ internal const string NullableContextDirective = "#nullable enable"; + } +} From 7507109a9e1c646ce23e774030a56f5605a7f1a9 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:43:01 -0700 Subject: [PATCH 06/19] Some extensions on base types for use in various components --- .../AccessibilityExtensions.cs | 20 ++++++ .../IndentedTextWriterExtensions.cs | 71 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/AccessibilityExtensions.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/IndentedTextWriterExtensions.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/AccessibilityExtensions.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/AccessibilityExtensions.cs new file mode 100644 index 000000000..fb80ebe87 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/AccessibilityExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; + +namespace Terminal.Gui.Analyzers.Internal; + +internal static class AccessibilityExtensions +{ + internal static string ToCSharpString (this Accessibility value) + { + return value switch + { + Accessibility.Public => "public", + Accessibility.Internal => "internal", + Accessibility.Private => "private", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.ProtectedOrInternal => "protected internal", + _ => string.Empty + }; + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/IndentedTextWriterExtensions.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/IndentedTextWriterExtensions.cs new file mode 100644 index 000000000..90105d582 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/IndentedTextWriterExtensions.cs @@ -0,0 +1,71 @@ +using System.CodeDom.Compiler; + +namespace Terminal.Gui.Analyzers.Internal; + +/// +/// Just a simple set of extension methods to increment and decrement the indentation +/// level of an via push and pop terms, and to avoid having +/// explicit values all over the place. +/// +public static class IndentedTextWriterExtensions +{ + /// + /// Decrements by 1, but only if it is greater than 0. + /// + /// + /// The resulting indentation level of the . + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static int Pop (this IndentedTextWriter w, string endScopeDelimiter = "}") + { + if (w.Indent > 0) + { + w.Indent--; + w.WriteLine (endScopeDelimiter); + } + return w.Indent; + } + + /// + /// Decrements by 1 and then writes a closing curly brace. + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static void PopCurly (this IndentedTextWriter w, bool withSemicolon = false) + { + w.Indent--; + + if (withSemicolon) + { + w.WriteLine ("};"); + } + else + { + w.WriteLine ('}'); + } + } + + /// + /// Increments by 1, with optional parameters to customize the scope push. + /// + /// An instance of an . + /// + /// The first line to be written before indenting and before the optional line or + /// null if not needed. + /// + /// + /// An opening delimiter to write. Written before the indentation and after (if provided). Default is an opening curly brace. + /// + /// Calling with no parameters will write an opening curly brace and a line break at the current indentation and then increment. + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static void Push (this IndentedTextWriter w, string? declaration = null, char scopeDelimiter = '{') + { + if (declaration is { Length: > 0 }) + { + w.WriteLine (declaration); + } + + w.WriteLine (scopeDelimiter); + + w.Indent++; + } +} From 86e8f6a1eccebb0b15c8918595e675b73eaa7d91 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:43:37 -0700 Subject: [PATCH 07/19] Conditionally-included polyfills for language feature support in netstandard2.0 --- .../CompilerFeatureRequiredAttribute.cs | 33 +++ .../Compatibility/IEqualityOperators.cs | 11 + .../Compatibility/IntrinsicAttribute.cs | 6 + .../Compatibility/IsExternalInit.cs | 18 ++ .../Compatibility/NullableAttributes.cs | 208 ++++++++++++++++++ .../Compatibility/NumericExtensions.cs | 43 ++++ .../Compatibility/RequiredMemberAttribute.cs | 12 + .../SetsRequiredMembersAttribute.cs | 12 + .../Compatibility/SkipLocalsInitAttribute.cs | 14 ++ 9 files changed, 357 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/CompilerFeatureRequiredAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IEqualityOperators.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IntrinsicAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IsExternalInit.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NullableAttributes.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NumericExtensions.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/RequiredMemberAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SetsRequiredMembersAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SkipLocalsInitAttribute.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/CompilerFeatureRequiredAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 000000000..cefddff94 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +/// +/// Indicates that compiler support for a particular feature is required for the location where this attribute is +/// applied. +/// +[AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] +internal sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute +{ + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof (RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof (RequiredMembers); + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } = featureName; + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not + /// understand . + /// + public bool IsOptional { get; init; } +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IEqualityOperators.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IEqualityOperators.cs new file mode 100644 index 000000000..63493a738 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IEqualityOperators.cs @@ -0,0 +1,11 @@ +// ReSharper disable once CheckNamespace +namespace System.Numerics; +/// +/// Included for compatibility with .net7+, but has no members. +/// Thus it cannot be explicitly used in generator code. +/// Use it for static analysis only. +/// +/// The left operand type. +/// The right operand type. +/// The return type. +internal interface IEqualityOperators; \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IntrinsicAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IntrinsicAttribute.cs new file mode 100644 index 000000000..06cd5b3d5 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IntrinsicAttribute.cs @@ -0,0 +1,6 @@ +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Field, Inherited = false)] +public sealed class IntrinsicAttribute : Attribute +{ +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IsExternalInit.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IsExternalInit.cs new file mode 100644 index 000000000..4976ee705 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/IsExternalInit.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable CheckNamespace +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +/// +/// Copied from .net source code, for support of init property accessors in netstandard2.0. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +[EditorBrowsable (EditorBrowsableState.Never)] +public static class IsExternalInit; diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NullableAttributes.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NullableAttributes.cs new file mode 100644 index 000000000..11ef7e4cf --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NullableAttributes.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +// This file is further modified from the original, for this project, +// to comply with project style. +// No changes are made which affect compatibility with the same types from +// APIs later than netstandard2.0, nor will this file be included in compilations +// targeted at later APIs. +// +// Originally rom https://github.com/dotnet/runtime/blob/ef72b95937703e485fdbbb75f3251fedfd1a0ef9/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// ReSharper disable CheckNamespace + +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable UnusedType.Global + +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies that null is allowed as an input even if the corresponding type disallows it. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class AllowNullAttribute : Attribute; + +/// Specifies that null is disallowed as an input even if the corresponding type allows it. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class DisallowNullAttribute : Attribute; + +/// Specifies that an output may be null even if the corresponding type disallows it. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class MaybeNullAttribute : Attribute; + +/// +/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input +/// argument was not null when the call returns. +/// +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class NotNullAttribute : Attribute; + +/// +/// Specifies that when a method returns , the parameter may be null even if the corresponding +/// type disallows it. +/// +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Parameter)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// +#pragma warning disable IDE0290 // Use primary constructor + public MaybeNullWhenAttribute (bool returnValue) { ReturnValue = returnValue; } +#pragma warning restore IDE0290 // Use primary constructor + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// +/// Specifies that when a method returns , the parameter will not be null even if the +/// corresponding type allows it. +/// +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Parameter)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// +#pragma warning disable IDE0290 // Use primary constructor + public NotNullWhenAttribute (bool returnValue) { ReturnValue = returnValue; } +#pragma warning restore IDE0290 // Use primary constructor + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +/// Specifies that the output will be non-null if the named parameter is non-null. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// +#pragma warning disable IDE0290 // Use primary constructor + public NotNullIfNotNullAttribute (string parameterName) { ParameterName = parameterName; } +#pragma warning restore IDE0290 // Use primary constructor + + /// Gets the associated parameter name. + public string ParameterName { get; } +} + +/// Applied to a method that will never return under any circumstance. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Method, Inherited = false)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class DoesNotReturnAttribute : Attribute; + +/// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Parameter)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument + /// to + /// the associated parameter matches this value. + /// +#pragma warning disable IDE0290 // Use primary constructor + public DoesNotReturnIfAttribute (bool parameterValue) { ParameterValue = parameterValue; } +#pragma warning restore IDE0290 // Use primary constructor + + /// Gets the condition parameter value. + public bool ParameterValue { get; } +} + +/// +/// Specifies that the method or property will ensure that the listed field and property members have not-null +/// values. +/// +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class MemberNotNullAttribute : Attribute +{ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute (string member) { Members = [member]; } + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute (params string [] members) { Members = members; } + + /// Gets field or property member names. + public string [] Members { get; } +} + +/// +/// Specifies that the method or property will ensure that the listed field and property members have not-null values +/// when returning with the specified return value condition. +/// +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +internal sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute (bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute (bool returnValue, params string [] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets field or property member names. + public string [] Members { get; } + + /// Gets the return value condition. + public bool ReturnValue { get; } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NumericExtensions.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NumericExtensions.cs new file mode 100644 index 000000000..8a6df7be9 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/NumericExtensions.cs @@ -0,0 +1,43 @@ +// ReSharper disable once CheckNamespace +namespace Terminal.Gui.Analyzers.Internal.Compatibility; + +/// +/// Extension methods for and types. +/// +/// +/// This is mostly just for backward compatibility with netstandard2.0. +/// +public static class NumericExtensions +{ + /// + /// Gets the population count (number of bits set to 1) of this 32-bit value. + /// + /// The value to get the population count of. + /// + /// The algorithm is the well-known SWAR (SIMD Within A Register) method for population count.
+ /// Included for hardware- and runtime- agnostic support for the equivalent of the x86 popcnt instruction, since + /// System.Numerics.Intrinsics isn't available in netstandard2.0.
+ /// It performs the operation simultaneously on 4 bytes at a time, rather than the naive method of testing all 32 bits + /// individually.
+ /// Most compilers can recognize this and turn it into a single platform-specific instruction, when available. + ///
+ /// + /// An unsigned 32-bit integer value containing the population count of . + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static uint GetPopCount (this uint value) + { + unchecked + { + value -= (value >> 1) & 0x55555555; + value = (value & 0x33333333) + ((value >> 2) & 0x33333333); + value = (value + (value >> 4)) & 0x0F0F0F0F; + + return (value * 0x01010101) >> 24; + } + } + + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static uint GetPopCount (this int value) { return GetPopCount (Unsafe.As (ref value)); } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/RequiredMemberAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/RequiredMemberAttribute.cs new file mode 100644 index 000000000..2fb8d3841 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/RequiredMemberAttribute.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace +// ReSharper disable ConditionalAnnotation + +using JetBrains.Annotations; + +namespace System.Runtime.CompilerServices; + +/// Polyfill to enable netstandard2.0 assembly to use the required keyword. +/// Excluded from output assembly via file specified in ApiCompatExcludeAttributesFile element in the project file. +[AttributeUsage (AttributeTargets.Property)] +[UsedImplicitly] +public sealed class RequiredMemberAttribute : Attribute; diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SetsRequiredMembersAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SetsRequiredMembersAttribute.cs new file mode 100644 index 000000000..ae09ae2cf --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SetsRequiredMembersAttribute.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that this constructor sets all required members for the current type, and callers +/// do not need to set any required members themselves. +/// +[AttributeUsage (AttributeTargets.Constructor)] +public sealed class SetsRequiredMembersAttribute : Attribute; \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SkipLocalsInitAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SkipLocalsInitAttribute.cs new file mode 100644 index 000000000..1eb617362 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Compatibility/SkipLocalsInitAttribute.cs @@ -0,0 +1,14 @@ +namespace System.Runtime.CompilerServices; + +[AttributeUsage ( + AttributeTargets.Class + | AttributeTargets.Constructor + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Method + | AttributeTargets.Module + | AttributeTargets.Property + | AttributeTargets.Struct, + Inherited = false)] + +internal sealed class SkipLocalsInitAttribute : Attribute; \ No newline at end of file From e3d8f476f8d75281b0be60507742a74637d05caa Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:44:38 -0700 Subject: [PATCH 08/19] Currently implemented generators and supporting types such as their attributes --- ...teEnumExtensionMethodsAttributeAnalyzer.cs | 117 +++++ .../AssemblyExtendedEnumTypeAttribute.cs | 26 + .../CombinationGroupingAttribute.cs | 22 + .../ExtensionsForEnumTypeAttribute.cs | 37 ++ .../GenerateEnumExtensionMethodsAttribute.cs | 110 +++++ ...GenerateEnumMemberCombinationsAttribute.cs | 110 +++++ .../IExtensionsForEnumTypeAttribute.cs | 14 + .../Generators/EnumExtensions/CodeWriter.cs | 235 +++++++++ .../EnumExtensionMethodsGenerationInfo.cs | 457 ++++++++++++++++++ ...numExtensionMethodsIncrementalGenerator.cs | 452 +++++++++++++++++ .../EnumMemberCombinationsGenerator.cs | 130 +++++ .../IGeneratedTypeMetadata.cs | 38 ++ .../IStandardCSharpCodeGenerator.cs | 28 ++ 13 files changed, 1776 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs new file mode 100644 index 000000000..d49fd37d1 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Analyzers/GenerateEnumExtensionMethodsAttributeAnalyzer.cs @@ -0,0 +1,117 @@ +#define JETBRAINS_ANNOTATIONS +using System.Collections.Immutable; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Terminal.Gui.Analyzers.Internal.Attributes; +using Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +namespace Terminal.Gui.Analyzers.Internal.Analyzers; + +/// +/// Design-time analyzer that checks for proper use of . +/// +[DiagnosticAnalyzer (LanguageNames.CSharp)] +[UsedImplicitly] +internal sealed class GenerateEnumExtensionMethodsAttributeAnalyzer : DiagnosticAnalyzer +{ + // ReSharper disable once InconsistentNaming + private static readonly DiagnosticDescriptor TG0001_GlobalNamespaceNotSupported = new ( + // ReSharper restore InconsistentNaming + "TG0001", + $"{nameof (GenerateEnumExtensionMethodsAttribute)} not supported on global enums", + "{0} is in the global namespace, which is not supported by the source generator ({1}) used by {2}. Move the enum to a namespace or remove the attribute.", + "Usage", + DiagnosticSeverity.Error, + true, + null, + null, + WellKnownDiagnosticTags.NotConfigurable, + WellKnownDiagnosticTags.Compiler); + + // ReSharper disable once InconsistentNaming + private static readonly DiagnosticDescriptor TG0002_UnderlyingTypeNotSupported = new ( + "TG0002", + $"{nameof (GenerateEnumExtensionMethodsAttribute)} not supported for this enum type", + "{0} has an underlying type of {1}, which is not supported by the source generator ({2}) used by {3}. Only enums backed by int or uint are supported.", + "Usage", + DiagnosticSeverity.Error, + true, + null, + null, + WellKnownDiagnosticTags.NotConfigurable, + WellKnownDiagnosticTags.Compiler); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + TG0001_GlobalNamespaceNotSupported, + TG0002_UnderlyingTypeNotSupported + ]; + + /// + public override void Initialize (AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution (); + + context.RegisterSyntaxNodeAction (CheckAttributeLocations, SyntaxKind.EnumDeclaration); + + return; + + static void CheckAttributeLocations (SyntaxNodeAnalysisContext analysisContext) + { + ISymbol? symbol = analysisContext.SemanticModel.GetDeclaredSymbol (analysisContext.Node) as INamedTypeSymbol; + + if (symbol is not INamedTypeSymbol { EnumUnderlyingType: { } } enumSymbol) + { + // Somehow not even an enum declaration. + // Skip it. + return; + } + + // Check attributes for those we care about and react accordingly. + foreach (AttributeData attributeData in enumSymbol.GetAttributes ()) + { + if (attributeData.AttributeClass?.Name != nameof (GenerateEnumExtensionMethodsAttribute)) + { + // Just skip - not an interesting attribute. + continue; + } + + // Check enum underlying type for supported types (int and uint, currently) + // Report TG0002 if unsupported underlying type. + if (enumSymbol.EnumUnderlyingType is not { SpecialType: SpecialType.System_Int32 or SpecialType.System_UInt32 }) + { + analysisContext.ReportDiagnostic ( + Diagnostic.Create ( + TG0002_UnderlyingTypeNotSupported, + enumSymbol.Locations.FirstOrDefault (), + enumSymbol.Name, + enumSymbol.EnumUnderlyingType.Name, + nameof (EnumExtensionMethodsIncrementalGenerator), + nameof (GenerateEnumExtensionMethodsAttribute) + ) + ); + } + + // Check enum namespace (only non-global supported, currently) + // Report TG0001 if in the global namespace. + if (enumSymbol.ContainingSymbol is not INamespaceSymbol { IsGlobalNamespace: false }) + { + analysisContext.ReportDiagnostic ( + Diagnostic.Create ( + TG0001_GlobalNamespaceNotSupported, + enumSymbol.Locations.FirstOrDefault (), + enumSymbol.Name, + nameof (EnumExtensionMethodsIncrementalGenerator), + nameof (GenerateEnumExtensionMethodsAttribute) + ) + ); + } + } + } + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs new file mode 100644 index 000000000..6115fdc46 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/AssemblyExtendedEnumTypeAttribute.cs @@ -0,0 +1,26 @@ +// ReSharper disable ClassNeverInstantiated.Global +#nullable enable + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// Assembly attribute declaring a known pairing of an type to an extension class. +/// This attribute should only be written by internal source generators for Terminal.Gui. No other usage of any kind is supported. +[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] +internal sealed class AssemblyExtendedEnumTypeAttribute : System.Attribute +{ + /// Creates a new instance of from the provided parameters. + /// The of an decorated with a . + /// The of the decorated with an referring to the same type as . + public AssemblyExtendedEnumTypeAttribute (System.Type enumType, System.Type extensionClass) + { + EnumType = enumType; + ExtensionClass = extensionClass; + } + ///An type that has been extended by Terminal.Gui source generators. + public System.Type EnumType { get; init; } + ///A class containing extension methods for . + public System.Type ExtensionClass { get; init; } + + /// + public override string ToString () => $"{EnumType.Name},{ExtensionClass.Name}"; +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs new file mode 100644 index 000000000..22d8eafd3 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/CombinationGroupingAttribute.cs @@ -0,0 +1,22 @@ +using System; +using JetBrains.Annotations; + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// +/// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with +/// this attribute which have the same value.
+///
+/// +/// This attribute is only considered for members of enum types which have the +/// . +/// +[AttributeUsage (AttributeTargets.Field)] +[UsedImplicitly] +internal sealed class CombinationGroupingAttribute : Attribute +{ + /// + /// Name of a group this member participates in, for FastHasFlags. + /// + public string GroupTag { get; set; } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs new file mode 100644 index 000000000..be4b6eef4 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/ExtensionsForEnumTypeAttribute.cs @@ -0,0 +1,37 @@ +// ReSharper disable RedundantNameQualifier +// ReSharper disable RedundantNullableDirective +// ReSharper disable UnusedType.Global +#pragma warning disable IDE0001, IDE0240 +#nullable enable + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// +/// Attribute written by the source generator for extension classes, for easier analysis and reflection. +/// +/// +/// Properties are just convenient shortcuts to properties of . +/// +[System.AttributeUsage (System.AttributeTargets.Class | System.AttributeTargets.Interface)] +internal sealed class ExtensionsForEnumTypeAttribute: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, System.Enum +{ + /// + /// The namespace-qualified name of . + /// + public string EnumFullName => EnumType.FullName!; + + /// + /// The unqualified name of . + /// + public string EnumName => EnumType.Name; + + /// + /// The namespace containing . + /// + public string EnumNamespace => EnumType.Namespace!; + + /// + /// The given by (). + /// + public System.Type EnumType => typeof (TEnum); +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs new file mode 100644 index 000000000..507c45102 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumExtensionMethodsAttribute.cs @@ -0,0 +1,110 @@ +// ReSharper disable RedundantNullableDirective +// ReSharper disable RedundantUsingDirective +// ReSharper disable ClassNeverInstantiated.Global + +#nullable enable +using System; +using Attribute = System.Attribute; +using AttributeUsageAttribute = System.AttributeUsageAttribute; +using AttributeTargets = System.AttributeTargets; + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// +/// Used to enable source generation of a common set of extension methods for enum types. +/// +[AttributeUsage (AttributeTargets.Enum)] +internal sealed class GenerateEnumExtensionMethodsAttribute : Attribute +{ + /// + /// The name of the generated static class. + /// + /// + /// If unspecified, null, empty, or only whitespace, defaults to the name of the enum plus "Extensions".
+ /// No other validation is performed, so illegal values will simply result in compiler errors. + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + ///
+ public string? ClassName { get; set; } + + /// + /// The namespace in which to place the generated static class containing the extension methods. + /// + /// + /// If unspecified, null, empty, or only whitespace, defaults to the namespace of the enum.
+ /// No other validation is performed, so illegal values will simply result in compiler errors. + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + ///
+ public string? ClassNamespace { get; set; } + + /// + /// Whether to generate a fast, zero-allocation, non-boxing, and reflection-free alternative to the built-in + /// method. + /// + /// + /// + /// Default: false + /// + /// + /// If the enum is not decorated with , this option has no effect. + /// + /// + /// If multiple members have the same value, the first member with that value will be used and subsequent members + /// with the same value will be skipped. + /// + /// + /// Overloads taking the enum type itself as well as the underlying type of the enum will be generated, enabling + /// avoidance of implicit or explicit cast overhead. + /// + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + /// + public bool FastHasFlags { get; set; } + + /// + /// Whether to generate a fast, zero-allocation, and reflection-free alternative to the built-in + /// method, + /// using a switch expression as a hard-coded reverse mapping of numeric values to explicitly-named members. + /// + /// + /// + /// Default: true + /// + /// + /// If multiple members have the same value, the first member with that value will be used and subsequent members + /// with the same value will be skipped. + /// + /// + /// As with the source generator only considers explicitly-named members.
+ /// Generation of values which represent valid bitwise combinations of members of enums decorated with + /// is not affected by this property. + ///
+ ///
+ public bool FastIsDefined { get; init; } = true; + + /// + /// Gets a value indicating if this instance + /// contains default values only. See remarks of this method or documentation on properties of this type for details. + /// + /// + /// A value indicating if all property values are default for this + /// instance. + /// + /// + /// Default values that will result in a return value are:
+ /// && ! && + /// && + /// + ///
+ public override bool IsDefaultAttribute () + { + return FastIsDefined + && !FastHasFlags + && ClassName is null + && ClassNamespace is null; + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs new file mode 100644 index 000000000..77d0c13a2 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/GenerateEnumMemberCombinationsAttribute.cs @@ -0,0 +1,110 @@ +// ReSharper disable RedundantUsingDirective + +using System; +using JetBrains.Annotations; +using Terminal.Gui.Analyzers.Internal.Compatibility; + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// +/// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with +/// this attribute which have the same value.
+///
+/// +/// +/// This attribute is only considered for enum types with the . +/// +/// +[AttributeUsage (AttributeTargets.Enum)] +[UsedImplicitly] +public sealed class GenerateEnumMemberCombinationsAttribute : System.Attribute +{ + private const byte MaximumPopCountLimit = 14; + private uint _mask; + private uint _maskPopCount; + private byte _popCountLimit = 8; + /// + public string GroupTag { get; set; } + + /// + /// The mask for the group defined in + /// + public uint Mask + { + get => _mask; + set + { +#if NET8_0_OR_GREATER + _maskPopCount = uint.PopCount (value); +#else + _maskPopCount = value.GetPopCount (); +#endif + PopCountLimitExceeded = _maskPopCount > PopCountLimit; + MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit; + + if (PopCountLimitExceeded || MaximumPopCountLimitExceeded) + { + return; + } + + _mask = value; + } + } + + /// + /// The maximum number of bits allowed to be set to 1 in . + /// + /// + /// + /// Default: 8 (256 possible combinations) + /// + /// + /// Increasing this value is not recommended!
+ /// Decreasing this value is pointless unless you want to limit maximum possible generated combinations even + /// further. + ///
+ /// + /// If the result of () exceeds 2 ^ + /// , no + /// combinations will be generated for the members which otherwise would have been included by . + /// Values exceeding the actual population count of have no effect. + /// + /// + /// This option is set to a sane default of 8, but also has a hard-coded limit of 14 (16384 combinations), as a + /// protection against generation of extremely large files. + /// + /// + /// CAUTION: The maximum number of possible combinations possible is equal to 1 << + /// (). + /// See for hard-coded limit, + /// + ///
+ public byte PopCountLimit + { + get => _popCountLimit; + set + { +#if NET8_0_OR_GREATER + _maskPopCount = uint.PopCount (_mask); +#else + _maskPopCount = _mask.GetPopCount (); +#endif + + PopCountLimitExceeded = _maskPopCount > value; + MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit; + + if (PopCountLimitExceeded || MaximumPopCountLimitExceeded) + { + return; + } + + _mask = value; + _popCountLimit = value; + } + } + + [UsedImplicitly] + internal bool MaximumPopCountLimitExceeded { get; private set; } + [UsedImplicitly] + internal bool PopCountLimitExceeded { get; private set; } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs new file mode 100644 index 000000000..4ae8875b7 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Attributes/IExtensionsForEnumTypeAttribute.cs @@ -0,0 +1,14 @@ +// ReSharper disable All + +using System; + +namespace Terminal.Gui.Analyzers.Internal.Attributes; + +/// +/// Interface to simplify general enumeration of constructed generic types for +/// +/// +internal interface IExtensionsForEnumTypeAttributes +{ + Type EnumType { get; } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs new file mode 100644 index 000000000..230d6293b --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/CodeWriter.cs @@ -0,0 +1,235 @@ +using System; +using System.CodeDom.Compiler; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using Microsoft.CodeAnalysis.Text; +using Terminal.Gui.Analyzers.Internal.Constants; + +namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +/// +/// The class responsible for turning an +/// into actual C# code. +/// +/// Try to use this type as infrequently as possible. +/// +/// A reference to an which will be used +/// to generate the extension class code. The object will not be validated, +/// so it is critical that it be correct and remain unchanged while in use +/// by an instance of this class. Behavior if those rules are not followed +/// is undefined. +/// +[SuppressMessage ("CodeQuality", "IDE0079", Justification = "Suppressions here are intentional and the warnings they disable are just noise.")] +internal sealed class CodeWriter (in EnumExtensionMethodsGenerationInfo metadata) : IStandardCSharpCodeGenerator +{ + // Using the null suppression operator here because this will always be + // initialized to non-null before a reference to it is returned. + private SourceText _sourceText = null!; + + /// + public EnumExtensionMethodsGenerationInfo Metadata + { + [MethodImpl (MethodImplOptions.AggressiveInlining)] + [return: NotNull] + get; + [param: DisallowNull] + set; + } = metadata; + + /// + public ref readonly SourceText GenerateSourceText (Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + _sourceText = SourceText.From (GetFullSourceText (), encoding); + + return ref _sourceText; + } + + /// + /// Gets the using directive for the namespace containing the enum, + /// if different from the extension class namespace, or an empty string, if they are the same. + /// + private string EnumNamespaceUsingDirective => Metadata.TargetTypeNamespace != Metadata.GeneratedTypeNamespace + + // ReSharper disable once HeapView.ObjectAllocation + ? $"using {Metadata.TargetTypeNamespace};" + : string.Empty; + + private string EnumTypeKeyword => Metadata.EnumBackingTypeCode switch + { + TypeCode.Int32 => "int", + TypeCode.UInt32 => "uint", + _ => string.Empty + }; + + /// Gets the class declaration line. + private string ExtensionClassDeclarationLine => $"public static partial class {Metadata.GeneratedTypeName}"; + + // ReSharper disable once HeapView.ObjectAllocation + /// Gets the XmlDoc for the extension class declaration. + private string ExtensionClassDeclarationXmlDoc => + $"/// Extension methods for the type."; + + // ReSharper disable once HeapView.ObjectAllocation + /// Gets the extension class file-scoped namespace directive. + private string ExtensionClassNamespaceDirective => $"namespace {Metadata.GeneratedTypeNamespace};"; + + /// + /// An attribute to decorate the extension class with for easy mapping back to the target enum type, for reflection and + /// analysis. + /// + private string ExtensionsForTypeAttributeLine => $"[ExtensionsForEnumType<{Metadata.TargetTypeFullName}>]"; + + /// + /// Creates the code for the FastHasFlags method. + /// + /// + /// Since the generator already only writes code for enums backed by and , + /// this method is safe, as we'll always be using a DWORD. + /// + /// An instance of an to write to. + private void GetFastHasFlagsMethods (IndentedTextWriter w) + { + // The version taking the same enum type as the check value. + w.WriteLine ( + $"/// Determines if the specified flags are set in the current value of this ."); + w.WriteLine ("/// NO VALIDATION IS PERFORMED!"); + + w.WriteLine ( + $"/// True, if all flags present in are also present in the current value of the .
Otherwise false.
"); + w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining); + + w.Push ( + $"{Metadata.Accessibility.ToCSharpString ()} static bool FastHasFlags (this {Metadata.TargetTypeFullName} e, {Metadata.TargetTypeFullName} checkFlags)"); + w.WriteLine ($"ref uint enumCurrentValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref e);"); + w.WriteLine ($"ref uint checkFlagsValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref checkFlags);"); + w.WriteLine ("return (enumCurrentValueRef & checkFlagsValueRef) == checkFlagsValueRef;"); + w.Pop (); + + // The version taking the underlying type of the enum as the check value. + w.WriteLine ( + $"/// Determines if the specified mask bits are set in the current value of this ."); + + w.WriteLine ( + $"/// The value to check against the value."); + w.WriteLine ("/// A mask to apply to the current value."); + + w.WriteLine ( + $"/// True, if all bits set to 1 in the mask are also set to 1 in the current value of the .
Otherwise false.
"); + w.WriteLine ("/// NO VALIDATION IS PERFORMED!"); + w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining); + + w.Push ( + $"{Metadata.Accessibility.ToCSharpString ()} static bool FastHasFlags (this {Metadata.TargetTypeFullName} e, {EnumTypeKeyword} mask)"); + w.WriteLine ($"ref {EnumTypeKeyword} enumCurrentValueRef = ref Unsafe.As<{Metadata.TargetTypeFullName},{EnumTypeKeyword}> (ref e);"); + w.WriteLine ("return (enumCurrentValueRef & mask) == mask;"); + w.Pop (); + } + + /// + /// Creates the code for the FastIsDefined method. + /// + [SuppressMessage ("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault", Justification = "Only need to handle int and uint.")] + [SuppressMessage ("ReSharper", "SwitchStatementMissingSomeEnumCasesNoDefault", Justification = "Only need to handle int and uint.")] + private void GetFastIsDefinedMethod (IndentedTextWriter w) + { + w.WriteLine ( + $"/// Determines if the specified value is explicitly defined as a named value of the type."); + + w.WriteLine ( + "/// Only explicitly named values return true, as with IsDefined. Combined valid flag values of flags enums which are not explicitly named will return false."); + + w.Push ( + $"{Metadata.Accessibility.ToCSharpString ()} static bool FastIsDefined (this {Metadata.TargetTypeFullName} e, {EnumTypeKeyword} value)"); + w.Push ("return value switch"); + + switch (Metadata.EnumBackingTypeCode) + { + case TypeCode.Int32: + foreach (int definedValue in Metadata.IntMembers) + { + w.WriteLine ($"{definedValue:D} => true,"); + } + + break; + case TypeCode.UInt32: + foreach (uint definedValue in Metadata.UIntMembers) + { + w.WriteLine ($"{definedValue:D} => true,"); + } + + break; + } + + w.WriteLine ("_ => false"); + + w.Pop ("};"); + w.Pop (); + } + + private string GetFullSourceText () + { + StringBuilder sb = new ( + $""" + {Strings.Templates.StandardHeader} + + [assembly: {Strings.AssemblyExtendedEnumTypeAttributeFullName} (typeof({Metadata.TargetTypeFullName}), typeof({Metadata.GeneratedTypeFullName}))] + + {EnumNamespaceUsingDirective} + {ExtensionClassNamespaceDirective} + {ExtensionClassDeclarationXmlDoc} + {Strings.Templates.AttributesForGeneratedTypes} + {ExtensionsForTypeAttributeLine} + {ExtensionClassDeclarationLine} + + """, + 4096); + + using IndentedTextWriter w = new (new StringWriter (sb)); + w.Push (); + + GetNamedValuesToInt32Method (w); + GetNamedValuesToUInt32Method (w); + + if (Metadata.GenerateFastIsDefined) + { + GetFastIsDefinedMethod (w); + } + + if (Metadata.GenerateFastHasFlags) + { + GetFastHasFlagsMethods (w); + } + + w.Pop (); + + w.Flush (); + + return sb.ToString (); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + private void GetNamedValuesToInt32Method (IndentedTextWriter w) + { + w.WriteLine ( + $"/// Directly converts this value to an value with the same binary representation."); + w.WriteLine ("/// NO VALIDATION IS PERFORMED!"); + w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining); + w.Push ($"{Metadata.Accessibility.ToCSharpString ()} static int AsInt32 (this {Metadata.TargetTypeFullName} e)"); + w.WriteLine ($"return Unsafe.As<{Metadata.TargetTypeFullName},int> (ref e);"); + w.Pop (); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + private void GetNamedValuesToUInt32Method (IndentedTextWriter w) + { + w.WriteLine ( + $"/// Directly converts this value to a value with the same binary representation."); + w.WriteLine ("/// NO VALIDATION IS PERFORMED!"); + w.WriteLine (Strings.DotnetNames.Attributes.Applications.AggressiveInlining); + w.Push ($"{Metadata.Accessibility.ToCSharpString ()} static uint AsUInt32 (this {Metadata.TargetTypeFullName} e)"); + w.WriteLine ($"return Unsafe.As<{Metadata.TargetTypeFullName},uint> (ref e);"); + w.Pop (); + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs new file mode 100644 index 000000000..970c51c35 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsGenerationInfo.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using JetBrains.Annotations; +using Microsoft.CodeAnalysis; +using Terminal.Gui.Analyzers.Internal.Attributes; +using Terminal.Gui.Analyzers.Internal.Constants; + +namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +/// +/// Type containing the information necessary to generate code according to the declared attribute values, +/// as well as the actual code to create the corresponding source code text, to be used in the +/// source generator pipeline. +/// +/// +/// Minimal validation is performed by this type.
+/// Errors in analyzed source code will result in generation failure or broken output.
+/// This type is not intended for use outside of Terminal.Gui library development. +///
+internal sealed record EnumExtensionMethodsGenerationInfo : IGeneratedTypeMetadata, + IEqualityOperators +{ + private const int ExplicitFastHasFlagsMask = 0b1000; + private const int ExplicitFastIsDefinedMask = 0b1_0000; + private const int ExplicitIncludeInterfaceMask = 0b10_0000; + private const int ExplicitNameMask = 0b10; + private const int ExplicitNamespaceMask = 0b1; + private const int ExplicitPartialMask = 0b100; + private const string GeneratorAttributeFullyQualifiedName = $"{GeneratorAttributeNamespace}.{GeneratorAttributeName}"; + private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute); + private const string GeneratorAttributeNamespace = Constants.Strings.AnalyzersAttributesNamespace; + + /// + /// Type containing the information necessary to generate code according to the declared attribute values, + /// as well as the actual code to create the corresponding source code text, to be used in the + /// source generator pipeline. + /// + /// The fully-qualified namespace of the enum type, without assembly name. + /// + /// The name of the enum type, as would be given by on the enum's type + /// declaration. + /// + /// + /// The fully-qualified namespace in which to place the generated code, without assembly name. If omitted or explicitly + /// null, uses the value provided in . + /// + /// + /// The name of the generated class. If omitted or explicitly null, appends "Extensions" to the value of + /// . + /// + /// The backing type of the enum. Defaults to . + /// + /// Whether to generate a fast HasFlag alternative. (Default: true) Ignored if the enum does not also have + /// . + /// + /// Whether to generate a fast IsDefined alternative. (Default: true) + /// + /// Minimal validation is performed by this type.
+ /// Errors in analyzed source code will result in generation failure or broken output.
+ /// This type is not intended for use outside of Terminal.Gui library development. + ///
+ public EnumExtensionMethodsGenerationInfo ( + string enumNamespace, + string enumTypeName, + string? typeNamespace = null, + string? typeName = null, + TypeCode enumBackingTypeCode = TypeCode.Int32, + bool generateFastHasFlags = true, + bool generateFastIsDefined = true + ) : this (enumNamespace, enumTypeName, enumBackingTypeCode) + { + GeneratedTypeNamespace = typeNamespace ?? enumNamespace; + GeneratedTypeName = typeName ?? string.Concat (enumTypeName, Strings.DefaultTypeNameSuffix); + GenerateFastHasFlags = generateFastHasFlags; + GenerateFastIsDefined = generateFastIsDefined; + } + + public EnumExtensionMethodsGenerationInfo (string enumNamespace, string enumTypeName, TypeCode enumBackingType) + { + // Interning these since they're rather unlikely to change. + string enumInternedNamespace = string.Intern (enumNamespace); + string enumInternedName = string.Intern (enumTypeName); + TargetTypeNamespace = enumInternedNamespace; + TargetTypeName = enumInternedName; + EnumBackingTypeCode = enumBackingType; + } + + [AccessedThroughProperty (nameof (EnumBackingTypeCode))] + private TypeCode _enumBackingTypeCode; + + [AccessedThroughProperty (nameof (GeneratedTypeName))] + private string? _generatedTypeName; + + [AccessedThroughProperty (nameof (GeneratedTypeNamespace))] + private string? _generatedTypeNamespace; + + private BitVector32 _discoveredProperties = new (0); + + /// The name of the extension class. + public string? GeneratedTypeName + { + get => _generatedTypeName ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix); + set => _generatedTypeName = value ?? string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix); + } + + /// The namespace for the extension class. + /// + /// Value is not validated by the set accessor.
+ /// Get accessor will never return null and is thus marked [NotNull] for static analysis, even though the property is + /// declared as a nullable .
If the backing field for this property is null, the get + /// accessor will return instead. + ///
+ public string? GeneratedTypeNamespace + { + get => _generatedTypeNamespace ?? TargetTypeNamespace; + set => _generatedTypeNamespace = value ?? TargetTypeNamespace; + } + + /// + public string TargetTypeFullName => string.Concat (TargetTypeNamespace, ".", TargetTypeName); + + /// + public Accessibility Accessibility + { + get; + [UsedImplicitly] + internal set; + } = Accessibility.Public; + + /// + public TypeKind TypeKind => TypeKind.Class; + + /// + public bool IsRecord => false; + + /// + public bool IsClass => true; + + /// + public bool IsStruct => false; + + /// + public bool IsByRefLike => false; + + /// + public bool IsSealed => false; + + /// + public bool IsAbstract => false; + + /// + public bool IsEnum => false; + + /// + public bool IsStatic => true; + + /// + public bool IncludeInterface { get; private set; } + + public string GeneratedTypeFullName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}"; + + /// Whether to generate the extension class as partial (Default: true) + public bool IsPartial => true; + + /// The fully-qualified namespace of the source enum type. + public string TargetTypeNamespace + { + get; + [UsedImplicitly] + set; + } + + /// The UNQUALIFIED name of the source enum type. + public string TargetTypeName + { + get; + [UsedImplicitly] + set; + } + + /// + /// The backing type for the enum. + /// + /// For simplicity and formality, only System.Int32 and System.UInt32 are supported at this time. + public TypeCode EnumBackingTypeCode + { + get => _enumBackingTypeCode; + set + { + if (value is not TypeCode.Int32 and not TypeCode.UInt32) + { + throw new NotSupportedException ("Only System.Int32 and System.UInt32 are supported at this time."); + } + + _enumBackingTypeCode = value; + } + } + + /// + /// Whether a fast alternative to the built-in Enum.HasFlag method will be generated (Default: false) + /// + public bool GenerateFastHasFlags { [UsedImplicitly] get; set; } + + /// Whether a switch-based IsDefined replacement will be generated (Default: true) + public bool GenerateFastIsDefined { [UsedImplicitly]get; set; } = true; + + internal ImmutableHashSet? IntMembers; + internal ImmutableHashSet? UIntMembers; + + /// + /// Fully-qualified name of the extension class + /// + internal string FullyQualifiedClassName => $"{GeneratedTypeNamespace}.{GeneratedTypeName}"; + + /// + /// Whether a Flags was found on the enum type. + /// + internal bool HasFlagsAttribute {[UsedImplicitly] get; set; } + + private static readonly SymbolDisplayFormat FullyQualifiedSymbolDisplayFormatWithoutGlobal = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle ( + SymbolDisplayGlobalNamespaceStyle.Omitted); + + + internal bool TryConfigure (INamedTypeSymbol enumSymbol, CancellationToken cancellationToken) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + cts.Token.ThrowIfCancellationRequested (); + + ImmutableArray attributes = enumSymbol.GetAttributes (); + + // This is theoretically impossible, but guarding just in case and canceling if it does happen. + if (attributes.Length == 0) + { + cts.Cancel (true); + + return false; + } + + // Check all attributes provided for anything interesting. + // Attributes can be in any order, so just check them all and adjust at the end if necessary. + // Note that we do not perform as strict validation on actual usage of the attribute, at this stage, + // because the analyzer should have already thrown errors for invalid uses like global namespace + // or unsupported enum underlying types. + foreach (AttributeData attr in attributes) + { + cts.Token.ThrowIfCancellationRequested (); + string? attributeFullyQualifiedName = attr.AttributeClass?.ToDisplayString (FullyQualifiedSymbolDisplayFormatWithoutGlobal); + + // Skip if null or not possibly an attribute we care about + if (attributeFullyQualifiedName is null or not { Length: >= 5 }) + { + continue; + } + + switch (attributeFullyQualifiedName) + { + // For Flags enums + case Strings.DotnetNames.Attributes.Flags: + { + HasFlagsAttribute = true; + } + + continue; + + // For the attribute that started this whole thing + case GeneratorAttributeFullyQualifiedName: + + { + // If we can't successfully complete this method, + // something is wrong enough that we may as well just stop now. + if (!TryConfigure (attr, cts.Token)) + { + if (cts.Token.CanBeCanceled) + { + cts.Cancel (); + } + + return false; + } + } + + continue; + } + } + + // Now get the members, if we know we'll need them. + if (GenerateFastIsDefined || GenerateFastHasFlags) + { + if (EnumBackingTypeCode == TypeCode.Int32) + { + PopulateIntMembersHashSet (enumSymbol); + } + else if (EnumBackingTypeCode == TypeCode.UInt32) + { + PopulateUIntMembersHashSet (enumSymbol); + } + } + + return true; + } + + private void PopulateIntMembersHashSet (INamedTypeSymbol enumSymbol) + { + ImmutableArray enumMembers = enumSymbol.GetMembers (); + IEnumerable fieldSymbols = enumMembers.OfType (); + IntMembers = fieldSymbols.Select (static m => m.HasConstantValue ? (int)m.ConstantValue : 0).ToImmutableHashSet (); + } + private void PopulateUIntMembersHashSet (INamedTypeSymbol enumSymbol) + { + UIntMembers = enumSymbol.GetMembers ().OfType ().Select (static m => (uint)m.ConstantValue).ToImmutableHashSet (); + } + + private bool HasExplicitFastHasFlags + { + [UsedImplicitly]get => _discoveredProperties [ExplicitFastHasFlagsMask]; + set => _discoveredProperties [ExplicitFastHasFlagsMask] = value; + } + + private bool HasExplicitFastIsDefined + { + [UsedImplicitly]get => _discoveredProperties [ExplicitFastIsDefinedMask]; + set => _discoveredProperties [ExplicitFastIsDefinedMask] = value; + } + + private bool HasExplicitIncludeInterface + { + [UsedImplicitly]get => _discoveredProperties [ExplicitIncludeInterfaceMask]; + set => _discoveredProperties [ExplicitIncludeInterfaceMask] = value; + } + + private bool HasExplicitPartial + { + [UsedImplicitly]get => _discoveredProperties [ExplicitPartialMask]; + set => _discoveredProperties [ExplicitPartialMask] = value; + } + + private bool HasExplicitTypeName + { + get => _discoveredProperties [ExplicitNameMask]; + set => _discoveredProperties [ExplicitNameMask] = value; + } + + private bool HasExplicitTypeNamespace + { + get => _discoveredProperties [ExplicitNamespaceMask]; + set => _discoveredProperties [ExplicitNamespaceMask] = value; + } + + [MemberNotNullWhen (true, nameof (_generatedTypeName), nameof (_generatedTypeNamespace))] + private bool TryConfigure (AttributeData attr, CancellationToken cancellationToken) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + cts.Token.ThrowIfCancellationRequested (); + + if (attr is not { NamedArguments.Length: > 0 }) + { + // Just a naked attribute, so configure with appropriate defaults. + GeneratedTypeNamespace = TargetTypeNamespace; + GeneratedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix); + + return true; + } + + cts.Token.ThrowIfCancellationRequested (); + + foreach (KeyValuePair kvp in attr.NamedArguments) + { + string? propName = kvp.Key; + TypedConstant propValue = kvp.Value; + + cts.Token.ThrowIfCancellationRequested (); + + // For every property name and value pair, set associated metadata + // property, if understood. + switch (propName, propValue) + { + // Null or empty string doesn't make sense, so skip if it happens. + case (null, _): + case ("", _): + continue; + + // ClassName is specified, not explicitly null, and at least 1 character long. + case (AttributeProperties.TypeNamePropertyName, { IsNull: false, Value: string { Length: > 1 } classNameProvidedValue }): + if (string.IsNullOrWhiteSpace (classNameProvidedValue)) + { + return false; + } + + GeneratedTypeName = classNameProvidedValue; + HasExplicitTypeName = true; + + continue; + + // Class namespace is specified, not explicitly null, and at least 1 character long. + case (AttributeProperties.TypeNamespacePropertyName, { IsNull: false, Value: string { Length: > 1 } classNamespaceProvidedValue }): + + if (string.IsNullOrWhiteSpace (classNamespaceProvidedValue)) + { + return false; + } + + GeneratedTypeNamespace = classNamespaceProvidedValue; + HasExplicitTypeNamespace = true; + + continue; + + // FastHasFlags is specified + case (AttributeProperties.FastHasFlagsPropertyName, { IsNull: false } fastHasFlagsConstant): + GenerateFastHasFlags = fastHasFlagsConstant.Value is true; + HasExplicitFastHasFlags = true; + + continue; + + // FastIsDefined is specified + case (AttributeProperties.FastIsDefinedPropertyName, { IsNull: false } fastIsDefinedConstant): + GenerateFastIsDefined = fastIsDefinedConstant.Value is true; + HasExplicitFastIsDefined = true; + + continue; + } + } + + // The rest is simple enough it's not really worth worrying about cancellation, so don't bother from here on... + + // Configure anything that wasn't specified that doesn't have an implicitly safe default + if (!HasExplicitTypeName || _generatedTypeName is null) + { + _generatedTypeName = string.Concat (TargetTypeName, Strings.DefaultTypeNameSuffix); + } + + if (!HasExplicitTypeNamespace || _generatedTypeNamespace is null) + { + _generatedTypeNamespace = TargetTypeNamespace; + } + + if (!HasFlagsAttribute) + { + GenerateFastHasFlags = false; + } + + return true; + } + + private static class AttributeProperties + { + internal const string FastHasFlagsPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastHasFlags); + internal const string FastIsDefinedPropertyName = nameof (GenerateEnumExtensionMethodsAttribute.FastIsDefined); + internal const string TypeNamePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassName); + internal const string TypeNamespacePropertyName = nameof (GenerateEnumExtensionMethodsAttribute.ClassNamespace); + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs new file mode 100644 index 000000000..66bfd3e81 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGenerator.cs @@ -0,0 +1,452 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Terminal.Gui.Analyzers.Internal.Attributes; +using Terminal.Gui.Analyzers.Internal.Constants; + +namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +/// +/// Incremental code generator for enums decorated with . +/// +[SuppressMessage ("CodeQuality", "IDE0079", Justification = "Suppressions here are intentional and the warnings they disable are just noise.")] +[Generator (LanguageNames.CSharp)] +public sealed class EnumExtensionMethodsIncrementalGenerator : IIncrementalGenerator +{ + private const string ExtensionsForEnumTypeAttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{ExtensionsForEnumTypeAttributeName}"; + private const string ExtensionsForEnumTypeAttributeName = "ExtensionsForEnumTypeAttribute"; + private const string GeneratorAttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{GeneratorAttributeName}"; + private const string GeneratorAttributeName = nameof (GenerateEnumExtensionMethodsAttribute); + + /// Fully-qualified symbol name format without the "global::" prefix. + private static readonly SymbolDisplayFormat FullyQualifiedSymbolDisplayFormatWithoutGlobal = + SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle (SymbolDisplayGlobalNamespaceStyle.Omitted); + + /// + /// + /// + /// Basically, this method is called once by the compiler, and is responsible for wiring up + /// everything important about how source generation works. + /// + /// + /// See in-line comments for specifics of what's going on. + /// + /// + /// Note that is everything in the compilation, + /// except for code generated by this generator or generators which have not yet executed.
+ /// The methods registered to perform generation get called on-demand by the host (the IDE, + /// compiler, etc), sometimes as often as every single keystroke. + ///
+ ///
+ public void Initialize (IncrementalGeneratorInitializationContext context) + { + // Write out namespaces that may be used later. Harmless to declare them now and will avoid + // additional processing and potential omissions later on. + context.RegisterPostInitializationOutput (GenerateDummyNamespaces); + + // This executes the delegate given to it immediately after Roslyn gets all set up. + // + // As written, this will result in the GenerateEnumExtensionMethodsAttribute code + // being added to the environment, so that it can be used without having to actually + // be declared explicitly in the target project. + // This is important, as it guarantees the type will exist and also guarantees it is + // defined exactly as the generator expects it to be defined. + context.RegisterPostInitializationOutput (GenerateAttributeSources); + + // Next up, we define our pipeline. + // To do so, we create one or more IncrementalValuesProvider objects, each of which + // defines on stage of analysis or generation as needed. + // + // Critically, these must be as fast and efficient as reasonably possible because, + // once the pipeline is registered, this stuff can get called A LOT. + // + // Note that declaring these doesn't really do much of anything unless they are given to the + // RegisterSourceOutput method at the end of this method. + // + // The delegates are not actually evaluated right here. That is triggered by changes being + // made to the source code. + + // This provider grabs attributes that pass our filter and then creates lightweight + // metadata objects to be used in the final code generation step. + // It also preemptively removes any nulls from the collection before handing things off + // to the code generation logic. + IncrementalValuesProvider enumGenerationInfos = + context + .SyntaxProvider + + // This method is a highly-optimized (and highly-recommended) filter on the incoming + // code elements that only bothers to present code that is annotated with the specified + // attribute, by its fully-qualified name, as a string, which is the first parameter. + // + // Two delegates are passed to it, in the second and third parameters. + // + // The second parameter is a filter predicate taking each SyntaxNode that passes the + // name filter above, and then refines that result. + // + // It is critical that the filter predicate be as simple and fast as possible, as it + // will be called a ton, triggered by keystrokes or anything else that modifies code + // in or even related to (in either direction) the pre-filtered code. + // It should collect metadata only and not actually generate any code. + // It must return a boolean indicating whether the supplied SyntaxNode should be + // given to the transform delegate at all. + // + // The third parameter is the "transform" delegate. + // That one only runs when code is changed that passed both the attribute name filter + // and the filter predicate in the second parameter. + // It will be called for everything that passes both of those, so it can still happen + // a lot, but should at least be pretty close. + // In our case, it should be 100% accurate, since we're using OUR attribute, which can + // only be applied to enum types in the first place. + // + // That delegate is responsible for creating some sort of lightweight data structure + // which can later be used to generate the actual source code for output. + // + // THIS DELEGATE DOES NOT GENERATE CODE! + // However, it does need to return instances of the metadata class in use that are either + // null or complete enough to generate meaningful code from, later on. + // + // We then filter out any that were null with the .Where call at the end, so that we don't + // know or care about them when it's time to generate code. + // + // While the syntax of that .Where call is the same as LINQ, that is actually a + // highly-optimized implementation specifically for this use. + .ForAttributeWithMetadataName ( + GeneratorAttributeFullyQualifiedName, + IsPotentiallyInterestingDeclaration, + GatherMetadataForCodeGeneration + ) + .WithTrackingName ("CollectEnumMetadata") + .Where (static eInfo => eInfo is { }); + + // Finally, we wire up any IncrementalValuesProvider instances above to the appropriate + // delegate that takes the SourceProductionContext that is current at run-time and an instance of + // our metadata type and takes appropriate action. + // Typically that means generating code from that metadata and adding it to the compilation via + // the received context object. + // + // As with everything else , the delegate will be invoked once for each item that passed + // all of the filters above, so we get to write that method from the perspective of a single + // enum type declaration. + + context.RegisterSourceOutput (enumGenerationInfos, GenerateSourceFromGenerationInfo); + } + + private static EnumExtensionMethodsGenerationInfo? GatherMetadataForCodeGeneration ( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken + ) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + cancellationToken.ThrowIfCancellationRequested (); + + // If it's not an enum symbol, we don't care. + // EnumUnderlyingType is null for non-enums, so this validates it's an enum declaration. + if (context.TargetSymbol is not INamedTypeSymbol { EnumUnderlyingType: { } } namedSymbol) + { + return null; + } + + INamespaceSymbol? enumNamespaceSymbol = namedSymbol.ContainingNamespace; + + if (enumNamespaceSymbol is null or { IsGlobalNamespace: true }) + { + // Explicitly choosing not to support enums in the global namespace. + // The corresponding analyzer will report this. + return null; + } + + string enumName = namedSymbol.Name; + + string enumNamespace = enumNamespaceSymbol.ToDisplayString (FullyQualifiedSymbolDisplayFormatWithoutGlobal); + + TypeCode enumTypeCode = namedSymbol.EnumUnderlyingType.Name switch + { + "UInt32" => TypeCode.UInt32, + "Int32" => TypeCode.Int32, + _ => TypeCode.Empty + }; + + EnumExtensionMethodsGenerationInfo info = new ( + enumNamespace, + enumName, + enumTypeCode + ); + + if (!info.TryConfigure (namedSymbol, cts.Token)) + { + cts.Cancel (); + cts.Token.ThrowIfCancellationRequested (); + } + + return info; + } + + + private static void GenerateAttributeSources (IncrementalGeneratorPostInitializationContext postInitializationContext) + { + postInitializationContext + .AddSource ( + $"{nameof (IExtensionsForEnumTypeAttributes)}.g.cs", + SourceText.From ( + $$""" + // ReSharper disable All + {{Strings.Templates.AutoGeneratedCommentBlock}} + using System; + + namespace {{Strings.AnalyzersAttributesNamespace}}; + + /// + /// Interface to simplify general enumeration of constructed generic types for + /// + /// + {{Strings.Templates.AttributesForGeneratedInterfaces}} + public interface IExtensionsForEnumTypeAttributes + { + System.Type EnumType { get; } + } + + """, + Encoding.UTF8)); + + postInitializationContext + .AddSource ( + $"{nameof (AssemblyExtendedEnumTypeAttribute)}.g.cs", + SourceText.From ( + $$""" + // ReSharper disable All + #nullable enable + {{Strings.Templates.AutoGeneratedCommentBlock}} + + namespace {{Strings.AnalyzersAttributesNamespace}}; + + /// Assembly attribute declaring a known pairing of an type to an extension class. + /// This attribute should only be written by internal source generators for Terminal.Gui. No other usage of any kind is supported. + {{Strings.Templates.AttributesForGeneratedTypes}} + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class {{nameof(AssemblyExtendedEnumTypeAttribute)}} : System.Attribute + { + /// Creates a new instance of from the provided parameters. + /// The of an decorated with a . + /// The of the decorated with an referring to the same type as . + public AssemblyExtendedEnumTypeAttribute (System.Type enumType, System.Type extensionClass) + { + EnumType = enumType; + ExtensionClass = extensionClass; + } + /// An type that has been extended by Terminal.Gui source generators. + public System.Type EnumType { get; init; } + /// A class containing extension methods for . + public System.Type ExtensionClass { get; init; } + /// + public override string ToString () => $"{EnumType.Name},{ExtensionClass.Name}"; + } + + """, + Encoding.UTF8)); + + postInitializationContext + .AddSource ( + $"{GeneratorAttributeFullyQualifiedName}.g.cs", + SourceText.From ( + $$""" + {{Strings.Templates.StandardHeader}} + + namespace {{Strings.AnalyzersAttributesNamespace}}; + + /// + /// Used to enable source generation of a common set of extension methods for enum types. + /// + {{Strings.Templates.AttributesForGeneratedTypes}} + [System.AttributeUsageAttribute (System.AttributeTargets.Enum)] + public sealed class {{GeneratorAttributeName}} : Attribute + { + /// + /// The name of the generated static class. + /// + /// + /// If unspecified, null, empty, or only whitespace, defaults to the name of the enum plus "Extensions".
+ /// No other validation is performed, so illegal values will simply result in compiler errors. + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + ///
+ public string? ClassName { get; set; } + + /// + /// The namespace in which to place the generated static class containing the extension methods. + /// + /// + /// If unspecified, null, empty, or only whitespace, defaults to the namespace of the enum.
+ /// No other validation is performed, so illegal values will simply result in compiler errors. + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + ///
+ public string? ClassNamespace { get; set; } + + /// + /// Whether to generate a fast, zero-allocation, non-boxing, and reflection-free alternative to the built-in + /// method. + /// + /// + /// + /// Default: false + /// + /// + /// If the enum is not decorated with , this option has no effect. + /// + /// + /// If multiple members have the same value, the first member with that value will be used and subsequent members + /// with the same value will be skipped. + /// + /// + /// Overloads taking the enum type itself as well as the underlying type of the enum will be generated, enabling + /// avoidance of implicit or explicit cast overhead. + /// + /// + /// Explicitly specifying a default value is unnecessary and will result in unnecessary processing. + /// + /// + public bool FastHasFlags { get; set; } + + /// + /// Whether to generate a fast, zero-allocation, and reflection-free alternative to the built-in + /// method, + /// using a switch expression as a hard-coded reverse mapping of numeric values to explicitly-named members. + /// + /// + /// + /// Default: true + /// + /// + /// If multiple members have the same value, the first member with that value will be used and subsequent members + /// with the same value will be skipped. + /// + /// + /// As with the source generator only considers explicitly-named members.
+ /// Generation of values which represent valid bitwise combinations of members of enums decorated with + /// is not affected by this property. + ///
+ ///
+ public bool FastIsDefined { get; init; } = true; + + /// + /// Gets a value indicating if this instance + /// contains default values only. See remarks of this method or documentation on properties of this type for details. + /// + /// + /// A value indicating if all property values are default for this + /// instance. + /// + /// + /// Default values that will result in a return value are:
+ /// && ! && + /// && + /// + ///
+ public override bool IsDefaultAttribute () + { + return FastIsDefined + && !FastHasFlags + && ClassName is null + && ClassNamespace is null; + } + } + + """, + Encoding.UTF8)); + + postInitializationContext + .AddSource ( + $"{ExtensionsForEnumTypeAttributeFullyQualifiedName}.g.cs", + SourceText.From ( + $$""" + // ReSharper disable RedundantNameQualifier + // ReSharper disable RedundantNullableDirective + // ReSharper disable UnusedType.Global + {{Strings.Templates.AutoGeneratedCommentBlock}} + #nullable enable + + namespace {{Strings.AnalyzersAttributesNamespace}}; + + /// + /// Attribute written by the source generator for enum extension classes, for easier analysis and reflection. + /// + /// + /// Properties are just convenient shortcuts to properties of . + /// + {{Strings.Templates.AttributesForGeneratedTypes}} + [System.AttributeUsageAttribute (System.AttributeTargets.Class | System.AttributeTargets.Interface)] + public sealed class {{ExtensionsForEnumTypeAttributeName}}: System.Attribute, IExtensionsForEnumTypeAttributes where TEnum : struct, Enum + { + /// + /// The namespace-qualified name of . + /// + public string EnumFullName => EnumType.FullName!; + + /// + /// The unqualified name of . + /// + public string EnumName => EnumType.Name; + + /// + /// The namespace containing . + /// + public string EnumNamespace => EnumType.Namespace!; + + /// + /// The given by (). + /// + public Type EnumType => typeof (TEnum); + } + + """, + Encoding.UTF8)); + } + + [SuppressMessage ("Roslynator", "RCS1267", Justification = "Intentionally used so that Spans are used.")] + private static void GenerateDummyNamespaces (IncrementalGeneratorPostInitializationContext postInitializeContext) + { + postInitializeContext.AddSource ( + string.Concat (Strings.InternalAnalyzersNamespace, "Namespaces.g.cs"), + SourceText.From (Strings.Templates.DummyNamespaceDeclarations, Encoding.UTF8)); + } + + private static void GenerateSourceFromGenerationInfo (SourceProductionContext context, EnumExtensionMethodsGenerationInfo? enumInfo) + { + // Just in case we still made it this far with a null... + if (enumInfo is not { }) + { + return; + } + + CodeWriter writer = new (enumInfo); + + context.AddSource ($"{enumInfo.FullyQualifiedClassName}.g.cs", writer.GenerateSourceText ()); + } + + /// + /// Returns true if is an EnumDeclarationSyntax + /// whose parent is a NamespaceDeclarationSyntax, FileScopedNamespaceDeclarationSyntax, or a + /// (Class|Struct)DeclarationSyntax.
+ /// Additional filtering is performed in later stages. + ///
+ private static bool IsPotentiallyInterestingDeclaration (SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested (); + + return syntaxNode is + { + RawKind: 8858, //(int)SyntaxKind.EnumDeclaration, + Parent.RawKind: 8845 //(int)SyntaxKind.FileScopedNamespaceDeclaration + or 8842 //(int)SyntaxKind.NamespaceDeclaration + or 8855 //(int)SyntaxKind.ClassDeclaration + or 8856 //(int)SyntaxKind.StructDeclaration + or 9068 //(int)SyntaxKind.RecordStructDeclaration + or 9063 //(int)SyntaxKind.RecordDeclaration + }; + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs new file mode 100644 index 000000000..82753d078 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Generators/EnumExtensions/EnumMemberCombinationsGenerator.cs @@ -0,0 +1,130 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Terminal.Gui.Analyzers.Internal.Attributes; +using Terminal.Gui.Analyzers.Internal.Constants; + +namespace Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +/// +/// Implementation of for types decorated with . +/// +[Generator] +internal sealed class EnumMemberCombinationsGenerator : IIncrementalGenerator +{ + private const string AttributeCodeText = $$""" + {{Strings.Templates.StandardHeader}} + + namespace {{Strings.AnalyzersAttributesNamespace}}; + + /// + /// Designates an enum member for inclusion in generation of bitwise combinations with other members decorated with + /// this attribute which have the same value.
+ ///
+ /// + /// + /// This attribute is only considered for enum types with the . + /// + /// + /// Masks with more than 8 bits set will + /// + /// + [AttributeUsageAttribute(AttributeTargets.Enum)] + internal sealed class {{nameof (GenerateEnumMemberCombinationsAttribute)}} : System.Attribute + { + public const byte MaximumPopCountLimit = 14; + private uint _mask; + private uint _maskPopCount; + private byte _popCountLimit = 8; + public required string GroupTag { get; set; } + + public required uint Mask + { + get => _mask; + set + { + _maskPopCount = uint.PopCount(value); + + PopCountLimitExceeded = _maskPopCount > PopCountLimit; + MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit; + + if (PopCountLimitExceeded || MaximumPopCountLimitExceeded) + { + return; + } + + _mask = value; + } + } + + /// + /// The maximum number of bits allowed to be set to 1 in . + /// + /// + /// + /// Default: 8 (256 possible combinations) + /// + /// + /// Increasing this value is not recommended!
+ /// Decreasing this value is pointless unless you want to limit maximum possible generated combinations even + /// further. + ///
+ /// + /// If the result of () exceeds 2 ^ , no + /// combinations will be generated for the members which otherwise would have been included by . + /// Values exceeding the actual population count of have no effect. + /// + /// + /// This option is set to a sane default of 8, but also has a hard-coded limit of 14 (16384 combinations), as a + /// protection against generation of extremely large files. + /// + /// + /// CAUTION: The maximum number of possible combinations possible is equal to 1 << + /// (). + /// See for hard-coded limit, + /// + ///
+ public byte PopCountLimit + { + get => _popCountLimit; + set + { + _maskPopCount = uint.PopCount(_mask); + + PopCountLimitExceeded = _maskPopCount > value; + MaximumPopCountLimitExceeded = _maskPopCount > MaximumPopCountLimit; + + if (PopCountLimitExceeded || MaximumPopCountLimitExceeded) + { + return; + } + + _mask = value; + _popCountLimit = value; + } + } + + internal bool MaximumPopCountLimitExceeded { get; private set; } + internal bool PopCountLimitExceeded { get; private set; } + } + + """; + + private const string AttributeFullyQualifiedName = $"{Strings.AnalyzersAttributesNamespace}.{AttributeName}"; + private const string AttributeName = "GenerateEnumMemberCombinationsAttribute"; + + /// + public void Initialize (IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput (GenerateAttributeCode); + + return; + + static void GenerateAttributeCode (IncrementalGeneratorPostInitializationContext initContext) + { +#pragma warning disable IDE0061 // Use expression body for local function + initContext.AddSource ($"{AttributeFullyQualifiedName}.g.cs", SourceText.From (AttributeCodeText, Encoding.UTF8)); +#pragma warning restore IDE0061 // Use expression body for local function + } + } +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs new file mode 100644 index 000000000..c72a8cc44 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/IGeneratedTypeMetadata.cs @@ -0,0 +1,38 @@ +using JetBrains.Annotations; +using Microsoft.CodeAnalysis; + +namespace Terminal.Gui.Analyzers.Internal; + +/// +/// Interface for all generators to use for their metadata classes. +/// +/// The type implementing this interface. +internal interface IGeneratedTypeMetadata where TSelf : IGeneratedTypeMetadata +{ + [UsedImplicitly] + string GeneratedTypeNamespace { get; } + [UsedImplicitly] + string? GeneratedTypeName { get; } + [UsedImplicitly] + string GeneratedTypeFullName { get; } + [UsedImplicitly] + string TargetTypeNamespace { get; } + [UsedImplicitly] + string TargetTypeName { get; } + string TargetTypeFullName { get; } + [UsedImplicitly] + Accessibility Accessibility { get; } + TypeKind TypeKind { get; } + bool IsRecord { get; } + bool IsClass { get; } + bool IsStruct { get; } + [UsedImplicitly] + bool IsPartial { get; } + bool IsByRefLike { get; } + bool IsSealed { get; } + bool IsAbstract { get; } + bool IsEnum { get; } + bool IsStatic { get; } + [UsedImplicitly] + bool IncludeInterface { get; } +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs b/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs new file mode 100644 index 000000000..a0e3d584d --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/IStandardCSharpCodeGenerator.cs @@ -0,0 +1,28 @@ +using System.Text; +using JetBrains.Annotations; +using Microsoft.CodeAnalysis.Text; + +namespace Terminal.Gui.Analyzers.Internal; + +internal interface IStandardCSharpCodeGenerator where T : IGeneratedTypeMetadata +{ + /// + /// Generates and returns the full source text corresponding to , + /// in the requested or if not provided. + /// + /// + /// The of the generated source text or if not + /// provided. + /// + /// + [UsedImplicitly] + [SkipLocalsInit] + ref readonly SourceText GenerateSourceText (Encoding? encoding = null); + + /// + /// A type implementing which + /// will be used for source generation. + /// + [UsedImplicitly] + T Metadata { get; set; } +} From bdaa375d512072a5491198e4260c98f6cdc95653 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:45:16 -0700 Subject: [PATCH 09/19] Filter for types that should not be included in the output assembly --- .../ApiCompatExcludedAttributes.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/ApiCompatExcludedAttributes.txt diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/ApiCompatExcludedAttributes.txt b/Analyzers/Terminal.Gui.Analyzers.Internal/ApiCompatExcludedAttributes.txt new file mode 100644 index 000000000..503f1f0bb --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/ApiCompatExcludedAttributes.txt @@ -0,0 +1,3 @@ +N:System.Runtime.CompilerServices +N:System.Diagnostics.CodeAnalysis +N:System.Numerics \ No newline at end of file From 15feab1e1138dd5fd83192e643aab6f0d3bb81ce Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:45:39 -0700 Subject: [PATCH 10/19] Specifications for currently implemented analyzers --- .../AnalyzerReleases.Shipped.md | 8 ++++++++ .../AnalyzerReleases.Unshipped.md | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Shipped.md create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Unshipped.md diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Shipped.md b/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..9316c42e0 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +TG0001 | Usage | Error | TG0001_GlobalNamespaceNotSupported +TG0002 | Usage | Error | TG0002_UnderlyingTypeNotSupported \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Unshipped.md b/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..cb4c8a8b9 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- From dc8d0bb165f389618110e4cb564b62368c50d54f Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:46:11 -0700 Subject: [PATCH 11/19] launchSettings used when debugging analyzer project --- .../Properties/launchSettings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal/Properties/launchSettings.json diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal/Properties/launchSettings.json b/Analyzers/Terminal.Gui.Analyzers.Internal/Properties/launchSettings.json new file mode 100644 index 000000000..639272733 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "InternalAnalyzers Debug": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Terminal.Gui.Analyzers.Internal.Debugging\\Terminal.Gui.Analyzers.Internal.Debugging.csproj" + } + } +} \ No newline at end of file From 9d650b0f7d7a46e9359b10589482c03494c40e26 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:46:26 -0700 Subject: [PATCH 12/19] Tests for extensions --- .../IndentedTextWriterExtensionsTests.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/IndentedTextWriterExtensionsTests.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/IndentedTextWriterExtensionsTests.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/IndentedTextWriterExtensionsTests.cs new file mode 100644 index 000000000..250971d58 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/IndentedTextWriterExtensionsTests.cs @@ -0,0 +1,111 @@ +using System.CodeDom.Compiler; +using System.Text; + +namespace Terminal.Gui.Analyzers.Internal.Tests; + +[TestFixture] +[Category ("Extension Methods")] +[TestOf (typeof (IndentedTextWriterExtensions))] +[Parallelizable (ParallelScope.Children)] +public class IndentedTextWriterExtensionsTests +{ + [Test] + public void Pop_Decrements () + { + StringBuilder sb = new (0); + using var sw = new StringWriter (sb); + using var writer = new IndentedTextWriter (sw); + writer.Indent = 5; + + Assume.That (writer.Indent, Is.EqualTo (5)); + + writer.Pop (); + Assert.That (writer.Indent, Is.EqualTo (4)); + } + + [Test] + public void Pop_WithClosing_WritesAndPops ([Values ("}", ")", "]")] string scopeClosing) + { + StringBuilder sb = new (256); + using var sw = new StringWriter (sb); + using var writer = new IndentedTextWriter (sw, " "); + writer.Indent = 5; + writer.Flush (); + Assume.That (writer.Indent, Is.EqualTo (5)); + Assume.That (sb.Length, Is.Zero); + + // Need to write something first, or IndentedTextWriter won't emit the indentation for the first call. + // So we'll write an empty line. + writer.WriteLine (); + + for (ushort indentCount = 5; indentCount > 0;) + { + writer.Pop (scopeClosing); + Assert.That (writer.Indent, Is.EqualTo (--indentCount)); + } + + writer.Flush (); + var result = sb.ToString (); + + Assert.That ( + result, + Is.EqualTo ( + $""" + + {scopeClosing} + {scopeClosing} + {scopeClosing} + {scopeClosing} + {scopeClosing} + + """)); + } + + [Test] + public void Push_Increments () + { + StringBuilder sb = new (32); + using var sw = new StringWriter (sb); + using var writer = new IndentedTextWriter (sw, " "); + + for (int indentCount = 0; indentCount < 5; indentCount++) + { + writer.Push (); + Assert.That (writer.Indent, Is.EqualTo (indentCount + 1)); + } + } + + [Test] + public void Push_WithOpening_WritesAndPushes ([Values ('{', '(', '[')] char scopeOpening) + { + StringBuilder sb = new (256); + using var sw = new StringWriter (sb); + using var writer = new IndentedTextWriter (sw, " "); + + for (ushort indentCount = 0; indentCount < 5;) + { + writer.Push ("Opening UninterestingEnum", scopeOpening); + Assert.That (writer.Indent, Is.EqualTo (++indentCount)); + } + + writer.Flush (); + var result = sb.ToString (); + + Assert.That ( + result, + Is.EqualTo ( + $""" + Opening UninterestingEnum + {scopeOpening} + Opening UninterestingEnum + {scopeOpening} + Opening UninterestingEnum + {scopeOpening} + Opening UninterestingEnum + {scopeOpening} + Opening UninterestingEnum + {scopeOpening} + + """)); + } +} From b47aebb934aa2cb67437b12c0bcfa6b32e20ec44 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:46:41 -0700 Subject: [PATCH 13/19] Definitions of enum types for use in tests --- .../EnumDefinitions/EnumMemberValues.cs | 48 +++++++++++++++++ .../WithGenerator/BetterEnum.cs | 49 ++++++++++++++++++ .../WithGenerator/BetterEnum_ExplicitInt.cs | 49 ++++++++++++++++++ .../BetterEnum_ExplicitInt_NoFastIsDefined.cs | 50 ++++++++++++++++++ .../WithGenerator/BetterEnum_ExplicitUInt.cs | 49 ++++++++++++++++++ ...BetterEnum_ExplicitUInt_NoFastIsDefined.cs | 49 ++++++++++++++++++ .../BetterEnum_NoFastIsDefined.cs | 49 ++++++++++++++++++ .../WithGenerator/BetterFlagsEnum.cs | 50 ++++++++++++++++++ .../BetterFlagsEnum_ExplicitInt.cs | 51 +++++++++++++++++++ .../BetterFlagsEnum_ExplicitUInt.cs | 50 ++++++++++++++++++ .../WithoutGenerator/BasicEnum.cs | 46 +++++++++++++++++ .../WithoutGenerator/BasicEnum_ExplicitInt.cs | 48 +++++++++++++++++ .../BasicEnum_ExplicitUint.cs | 46 +++++++++++++++++ .../WithoutGenerator/FlagsEnum.cs | 43 ++++++++++++++++ .../WithoutGenerator/FlagsEnum_ExplicitInt.cs | 43 ++++++++++++++++ .../FlagsEnum_ExplicitUInt.cs | 43 ++++++++++++++++ 16 files changed, 763 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/EnumMemberValues.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt_NoFastIsDefined.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt_NoFastIsDefined.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_NoFastIsDefined.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitUInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitUint.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitInt.cs create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitUInt.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/EnumMemberValues.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/EnumMemberValues.cs new file mode 100644 index 000000000..6f7ac613f --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/EnumMemberValues.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; +internal class SignedEnumMemberValues +{ + internal const int Bit31 = ~0b_01111111_11111111_11111111_11111111; + internal const int Bit30 = 0b_01000000_00000000_00000000_00000000; + internal const int Bit29 = 0b_00100000_00000000_00000000_00000000; + internal const int Bit28 = 0b_00010000_00000000_00000000_00000000; + internal const int Bit27 = 0b_00001000_00000000_00000000_00000000; + internal const int Bit26 = 0b_00000100_00000000_00000000_00000000; + internal const int Bit25 = 0b_00000010_00000000_00000000_00000000; + internal const int Bit24 = 0b_00000001_00000000_00000000_00000000; + internal const int Bit23 = 0b_00000000_10000000_00000000_00000000; + internal const int Bit22 = 0b_00000000_01000000_00000000_00000000; + internal const int Bit21 = 0b_00000000_00100000_00000000_00000000; + internal const int Bit20 = 0b_00000000_00010000_00000000_00000000; + internal const int Bit19 = 0b_00000000_00001000_00000000_00000000; + internal const int Bit18 = 0b_00000000_00000100_00000000_00000000; + internal const int Bit17 = 0b_00000000_00000010_00000000_00000000; + internal const int Bit16 = 0b_00000000_00000001_00000000_00000000; + internal const int Bit15 = 0b_00000000_00000000_10000000_00000000; + internal const int Bit14 = 0b_00000000_00000000_01000000_00000000; + internal const int Bit13 = 0b_00000000_00000000_00100000_00000000; + internal const int Bit12 = 0b_00000000_00000000_00010000_00000000; + internal const int Bit11 = 0b_00000000_00000000_00001000_00000000; + internal const int Bit10 = 0b_00000000_00000000_00000100_00000000; + internal const int Bit09 = 0b_00000000_00000000_00000010_00000000; + internal const int Bit08 = 0b_00000000_00000000_00000001_00000000; + internal const int Bit07 = 0b_00000000_00000000_00000000_10000000; + internal const int Bit06 = 0b_00000000_00000000_00000000_01000000; + internal const int Bit05 = 0b_00000000_00000000_00000000_00100000; + internal const int Bit04 = 0b_00000000_00000000_00000000_00010000; + internal const int Bit03 = 0b_00000000_00000000_00000000_00001000; + internal const int Bit02 = 0b_00000000_00000000_00000000_00000100; + internal const int Bit01 = 0b_00000000_00000000_00000000_00000010; + internal const int Bit00 = 0b_00000000_00000000_00000000_00000001; + internal const int All_0 = 0; + internal const int All_1 = ~All_0; + internal const int Alternating_01 = 0b_01010101_01010101_01010101_01010101; + internal const int Alternating_10 = ~Alternating_01; + internal const int EvenBytesHigh = 0b_00000000_11111111_00000000_11111111; + internal const int OddBytesHigh = ~EvenBytesHigh; +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum.cs new file mode 100644 index 000000000..0a57e7dc3 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum.cs @@ -0,0 +1,49 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with applied. +/// +[GenerateEnumExtensionMethods] +public enum BetterEnum +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = 0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = 0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = 0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt.cs new file mode 100644 index 000000000..7514e1b66 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt.cs @@ -0,0 +1,49 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with applied. +/// +[GenerateEnumExtensionMethods] +public enum BetterEnum_ExplicitInt +{ + Bit31 = BasicEnum_ExplicitInt.Bit31, + Bit30 = BasicEnum_ExplicitInt.Bit30, + Bit29 = BasicEnum_ExplicitInt.Bit29, + Bit28 = BasicEnum_ExplicitInt.Bit28, + Bit27 = BasicEnum_ExplicitInt.Bit27, + Bit26 = BasicEnum_ExplicitInt.Bit26, + Bit25 = BasicEnum_ExplicitInt.Bit25, + Bit24 = BasicEnum_ExplicitInt.Bit24, + Bit23 = BasicEnum_ExplicitInt.Bit23, + Bit22 = BasicEnum_ExplicitInt.Bit22, + Bit21 = BasicEnum_ExplicitInt.Bit21, + Bit20 = BasicEnum_ExplicitInt.Bit20, + Bit19 = BasicEnum_ExplicitInt.Bit19, + Bit18 = BasicEnum_ExplicitInt.Bit18, + Bit17 = BasicEnum_ExplicitInt.Bit17, + Bit16 = BasicEnum_ExplicitInt.Bit16, + Bit15 = BasicEnum_ExplicitInt.Bit15, + Bit14 = BasicEnum_ExplicitInt.Bit14, + Bit13 = BasicEnum_ExplicitInt.Bit13, + Bit12 = BasicEnum_ExplicitInt.Bit12, + Bit11 = BasicEnum_ExplicitInt.Bit11, + Bit10 = BasicEnum_ExplicitInt.Bit10, + Bit09 = BasicEnum_ExplicitInt.Bit09, + Bit08 = BasicEnum_ExplicitInt.Bit08, + Bit07 = BasicEnum_ExplicitInt.Bit07, + Bit06 = BasicEnum_ExplicitInt.Bit06, + Bit05 = BasicEnum_ExplicitInt.Bit05, + Bit04 = BasicEnum_ExplicitInt.Bit04, + Bit03 = BasicEnum_ExplicitInt.Bit03, + Bit02 = BasicEnum_ExplicitInt.Bit02, + Bit01 = BasicEnum_ExplicitInt.Bit01, + Bit00 = BasicEnum_ExplicitInt.Bit00, + All_0 = BasicEnum_ExplicitInt.All_0, + All_1 = BasicEnum_ExplicitInt.All_1, + Alternating_01 = BasicEnum_ExplicitInt.Alternating_01, + Alternating_10 = BasicEnum_ExplicitInt.Alternating_10, + EvenBytesHigh = BasicEnum_ExplicitInt.EvenBytesHigh, + OddBytesHigh = BasicEnum_ExplicitInt.OddBytesHigh +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt_NoFastIsDefined.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt_NoFastIsDefined.cs new file mode 100644 index 000000000..0309c174e --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitInt_NoFastIsDefined.cs @@ -0,0 +1,50 @@ +// ReSharper disable EnumUnderlyingTypeIsInt +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with = . +/// +[GenerateEnumExtensionMethods (FastIsDefined = false)] +public enum BetterEnum_ExplicitInt_NoFastIsDefined : int +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = 0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = 0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = 0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt.cs new file mode 100644 index 000000000..175a8e440 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt.cs @@ -0,0 +1,49 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with applied. +/// +[GenerateEnumExtensionMethods] +public enum BetterEnum_ExplicitUInt : uint +{ + Bit31 = 0b_10000000_00000000_00000000_00000000u, + Bit30 = 0b_01000000_00000000_00000000_00000000u, + Bit29 = 0b_00100000_00000000_00000000_00000000u, + Bit28 = 0b_00010000_00000000_00000000_00000000u, + Bit27 = 0b_00001000_00000000_00000000_00000000u, + Bit26 = 0b_00000100_00000000_00000000_00000000u, + Bit25 = 0b_00000010_00000000_00000000_00000000u, + Bit24 = 0b_00000001_00000000_00000000_00000000u, + Bit23 = 0b_00000000_10000000_00000000_00000000u, + Bit22 = 0b_00000000_01000000_00000000_00000000u, + Bit21 = 0b_00000000_00100000_00000000_00000000u, + Bit20 = 0b_00000000_00010000_00000000_00000000u, + Bit19 = 0b_00000000_00001000_00000000_00000000u, + Bit18 = 0b_00000000_00000100_00000000_00000000u, + Bit17 = 0b_00000000_00000010_00000000_00000000u, + Bit16 = 0b_00000000_00000001_00000000_00000000u, + Bit15 = 0b_00000000_00000000_10000000_00000000u, + Bit14 = 0b_00000000_00000000_01000000_00000000u, + Bit13 = 0b_00000000_00000000_00100000_00000000u, + Bit12 = 0b_00000000_00000000_00010000_00000000u, + Bit11 = 0b_00000000_00000000_00001000_00000000u, + Bit10 = 0b_00000000_00000000_00000100_00000000u, + Bit09 = 0b_00000000_00000000_00000010_00000000u, + Bit08 = 0b_00000000_00000000_00000001_00000000u, + Bit07 = 0b_00000000_00000000_00000000_10000000u, + Bit06 = 0b_00000000_00000000_00000000_01000000u, + Bit05 = 0b_00000000_00000000_00000000_00100000u, + Bit04 = 0b_00000000_00000000_00000000_00010000u, + Bit03 = 0b_00000000_00000000_00000000_00001000u, + Bit02 = 0b_00000000_00000000_00000000_00000100u, + Bit01 = 0b_00000000_00000000_00000000_00000010u, + Bit00 = 0b_00000000_00000000_00000000_00000001u, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt_NoFastIsDefined.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt_NoFastIsDefined.cs new file mode 100644 index 000000000..bec88097b --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_ExplicitUInt_NoFastIsDefined.cs @@ -0,0 +1,49 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with = . +/// +[GenerateEnumExtensionMethods (FastIsDefined = false)] +public enum BetterEnum_ExplicitUInt_NoFastIsDefined : uint +{ + Bit31 = 0b_10000000_00000000_00000000_00000000u, + Bit30 = 0b_01000000_00000000_00000000_00000000u, + Bit29 = 0b_00100000_00000000_00000000_00000000u, + Bit28 = 0b_00010000_00000000_00000000_00000000u, + Bit27 = 0b_00001000_00000000_00000000_00000000u, + Bit26 = 0b_00000100_00000000_00000000_00000000u, + Bit25 = 0b_00000010_00000000_00000000_00000000u, + Bit24 = 0b_00000001_00000000_00000000_00000000u, + Bit23 = 0b_00000000_10000000_00000000_00000000u, + Bit22 = 0b_00000000_01000000_00000000_00000000u, + Bit21 = 0b_00000000_00100000_00000000_00000000u, + Bit20 = 0b_00000000_00010000_00000000_00000000u, + Bit19 = 0b_00000000_00001000_00000000_00000000u, + Bit18 = 0b_00000000_00000100_00000000_00000000u, + Bit17 = 0b_00000000_00000010_00000000_00000000u, + Bit16 = 0b_00000000_00000001_00000000_00000000u, + Bit15 = 0b_00000000_00000000_10000000_00000000u, + Bit14 = 0b_00000000_00000000_01000000_00000000u, + Bit13 = 0b_00000000_00000000_00100000_00000000u, + Bit12 = 0b_00000000_00000000_00010000_00000000u, + Bit11 = 0b_00000000_00000000_00001000_00000000u, + Bit10 = 0b_00000000_00000000_00000100_00000000u, + Bit09 = 0b_00000000_00000000_00000010_00000000u, + Bit08 = 0b_00000000_00000000_00000001_00000000u, + Bit07 = 0b_00000000_00000000_00000000_10000000u, + Bit06 = 0b_00000000_00000000_00000000_01000000u, + Bit05 = 0b_00000000_00000000_00000000_00100000u, + Bit04 = 0b_00000000_00000000_00000000_00010000u, + Bit03 = 0b_00000000_00000000_00000000_00001000u, + Bit02 = 0b_00000000_00000000_00000000_00000100u, + Bit01 = 0b_00000000_00000000_00000000_00000010u, + Bit00 = 0b_00000000_00000000_00000000_00000001u, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_NoFastIsDefined.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_NoFastIsDefined.cs new file mode 100644 index 000000000..2258d8a99 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterEnum_NoFastIsDefined.cs @@ -0,0 +1,49 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with = . +/// +[GenerateEnumExtensionMethods (FastIsDefined = false)] +public enum BetterEnum_NoFastIsDefined +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = 0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = 0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = 0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum.cs new file mode 100644 index 000000000..ffb50b098 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum.cs @@ -0,0 +1,50 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with applied. +/// +[Flags] +[GenerateEnumExtensionMethods] +public enum BetterFlagsEnum +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = -0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = -0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = -0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitInt.cs new file mode 100644 index 000000000..e244a3aa5 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitInt.cs @@ -0,0 +1,51 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// +/// Same as , but with applied. +/// +[Flags] +[GenerateEnumExtensionMethods] +public enum BetterFlagsEnum_ExplicitInt : int +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = -0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = -0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = -0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitUInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitUInt.cs new file mode 100644 index 000000000..89229a522 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithGenerator/BetterFlagsEnum_ExplicitUInt.cs @@ -0,0 +1,50 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Same as , but with applied. +/// +[Flags] +[GenerateEnumExtensionMethods] +public enum BetterFlagsEnum_ExplicitUInt : uint +{ + Bit31 = 0b_10000000_00000000_00000000_00000000u, + Bit30 = 0b_01000000_00000000_00000000_00000000u, + Bit29 = 0b_00100000_00000000_00000000_00000000u, + Bit28 = 0b_00010000_00000000_00000000_00000000u, + Bit27 = 0b_00001000_00000000_00000000_00000000u, + Bit26 = 0b_00000100_00000000_00000000_00000000u, + Bit25 = 0b_00000010_00000000_00000000_00000000u, + Bit24 = 0b_00000001_00000000_00000000_00000000u, + Bit23 = 0b_00000000_10000000_00000000_00000000u, + Bit22 = 0b_00000000_01000000_00000000_00000000u, + Bit21 = 0b_00000000_00100000_00000000_00000000u, + Bit20 = 0b_00000000_00010000_00000000_00000000u, + Bit19 = 0b_00000000_00001000_00000000_00000000u, + Bit18 = 0b_00000000_00000100_00000000_00000000u, + Bit17 = 0b_00000000_00000010_00000000_00000000u, + Bit16 = 0b_00000000_00000001_00000000_00000000u, + Bit15 = 0b_00000000_00000000_10000000_00000000u, + Bit14 = 0b_00000000_00000000_01000000_00000000u, + Bit13 = 0b_00000000_00000000_00100000_00000000u, + Bit12 = 0b_00000000_00000000_00010000_00000000u, + Bit11 = 0b_00000000_00000000_00001000_00000000u, + Bit10 = 0b_00000000_00000000_00000100_00000000u, + Bit09 = 0b_00000000_00000000_00000010_00000000u, + Bit08 = 0b_00000000_00000000_00000001_00000000u, + Bit07 = 0b_00000000_00000000_00000000_10000000u, + Bit06 = 0b_00000000_00000000_00000000_01000000u, + Bit05 = 0b_00000000_00000000_00000000_00100000u, + Bit04 = 0b_00000000_00000000_00000000_00010000u, + Bit03 = 0b_00000000_00000000_00000000_00001000u, + Bit02 = 0b_00000000_00000000_00000000_00000100u, + Bit01 = 0b_00000000_00000000_00000000_00000010u, + Bit00 = 0b_00000000_00000000_00000000_00000001u, + All_0 = 0, + All_1 = ~All_0, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = ~Alternating_01, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, + OddBytesHigh = ~EvenBytesHigh, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum.cs new file mode 100644 index 000000000..b49efcd78 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum.cs @@ -0,0 +1,46 @@ +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Basic enum without explicitly-defined backing type and no attributes on the enum or any of its members. +/// +public enum BasicEnum +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = 0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = 0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = 0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = -1, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = unchecked((int)0b_10101010_10101010_10101010_10101010), + OddBytesHigh = unchecked((int)0b_11111111_00000000_11111111_00000000), + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitInt.cs new file mode 100644 index 000000000..ee059cfac --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitInt.cs @@ -0,0 +1,48 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Basic enum with explicitly-defined backing type of int and no attributes on the enum or any of its members. +/// +public enum BasicEnum_ExplicitInt : int +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = 0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = 0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = 0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = -1, + Alternating_01 = 0b_01010101_01010101_01010101_01010101, + Alternating_10 = unchecked((int)0b_10101010_10101010_10101010_10101010), + OddBytesHigh = unchecked((int)0b_11111111_00000000_11111111_00000000), + EvenBytesHigh = unchecked((int)0b_00000000_11111111_00000000_11111111), +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitUint.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitUint.cs new file mode 100644 index 000000000..5a0ecd6c7 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/BasicEnum_ExplicitUint.cs @@ -0,0 +1,46 @@ +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Basic enum with explicitly-defined backing type of uint and no attributes on the enum or any of its members. +/// +public enum BasicEnum_ExplicitUInt : uint +{ + Bit31 = 0b_10000000_00000000_00000000_00000000u, + Bit30 = 0b_01000000_00000000_00000000_00000000u, + Bit29 = 0b_00100000_00000000_00000000_00000000u, + Bit28 = 0b_00010000_00000000_00000000_00000000u, + Bit27 = 0b_00001000_00000000_00000000_00000000u, + Bit26 = 0b_00000100_00000000_00000000_00000000u, + Bit25 = 0b_00000010_00000000_00000000_00000000u, + Bit24 = 0b_00000001_00000000_00000000_00000000u, + Bit23 = 0b_00000000_10000000_00000000_00000000u, + Bit22 = 0b_00000000_01000000_00000000_00000000u, + Bit21 = 0b_00000000_00100000_00000000_00000000u, + Bit20 = 0b_00000000_00010000_00000000_00000000u, + Bit19 = 0b_00000000_00001000_00000000_00000000u, + Bit18 = 0b_00000000_00000100_00000000_00000000u, + Bit17 = 0b_00000000_00000010_00000000_00000000u, + Bit16 = 0b_00000000_00000001_00000000_00000000u, + Bit15 = 0b_00000000_00000000_10000000_00000000u, + Bit14 = 0b_00000000_00000000_01000000_00000000u, + Bit13 = 0b_00000000_00000000_00100000_00000000u, + Bit12 = 0b_00000000_00000000_00010000_00000000u, + Bit11 = 0b_00000000_00000000_00001000_00000000u, + Bit10 = 0b_00000000_00000000_00000100_00000000u, + Bit09 = 0b_00000000_00000000_00000010_00000000u, + Bit08 = 0b_00000000_00000000_00000001_00000000u, + Bit07 = 0b_00000000_00000000_00000000_10000000u, + Bit06 = 0b_00000000_00000000_00000000_01000000u, + Bit05 = 0b_00000000_00000000_00000000_00100000u, + Bit04 = 0b_00000000_00000000_00000000_00010000u, + Bit03 = 0b_00000000_00000000_00000000_00001000u, + Bit02 = 0b_00000000_00000000_00000000_00000100u, + Bit01 = 0b_00000000_00000000_00000000_00000010u, + Bit00 = 0b_00000000_00000000_00000000_00000001u, + All_0 = 0b_00000000_00000000_00000000_00000000u, + All_1 = 0b_11111111_11111111_11111111_11111111u, + Alternating_01 = 0b_01010101_01010101_01010101_01010101u, + Alternating_10 = 0b_10101010_10101010_10101010_10101010u, + OddBytesHigh = 0b_11111111_00000000_11111111_00000000u, + EvenBytesHigh = 0b_00000000_11111111_00000000_11111111u, +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum.cs new file mode 100644 index 000000000..595175400 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum.cs @@ -0,0 +1,43 @@ +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Flags enum without explicitly-defined backing type and only a on the enum declaration No other attributes on the enum or its members.. +/// +[Flags] +public enum FlagsEnum +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = -0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = -0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = -0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = -1 +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitInt.cs new file mode 100644 index 000000000..cdd121606 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitInt.cs @@ -0,0 +1,43 @@ +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Flags enum with explicitly-defined backing type of int and only a on the enum declaration No other attributes on the enum or its members.. +/// +[Flags] +public enum FlagsEnum_ExplicitInt : int +{ + Bit31 = -0b_10000000_00000000_00000000_00000000, + Bit30 = 0b_01000000_00000000_00000000_00000000, + Bit29 = 0b_00100000_00000000_00000000_00000000, + Bit28 = 0b_00010000_00000000_00000000_00000000, + Bit27 = 0b_00001000_00000000_00000000_00000000, + Bit26 = 0b_00000100_00000000_00000000_00000000, + Bit25 = 0b_00000010_00000000_00000000_00000000, + Bit24 = 0b_00000001_00000000_00000000_00000000, + Bit23 = -0b_00000000_10000000_00000000_00000000, + Bit22 = 0b_00000000_01000000_00000000_00000000, + Bit21 = 0b_00000000_00100000_00000000_00000000, + Bit20 = 0b_00000000_00010000_00000000_00000000, + Bit19 = 0b_00000000_00001000_00000000_00000000, + Bit18 = 0b_00000000_00000100_00000000_00000000, + Bit17 = 0b_00000000_00000010_00000000_00000000, + Bit16 = 0b_00000000_00000001_00000000_00000000, + Bit15 = -0b_00000000_00000000_10000000_00000000, + Bit14 = 0b_00000000_00000000_01000000_00000000, + Bit13 = 0b_00000000_00000000_00100000_00000000, + Bit12 = 0b_00000000_00000000_00010000_00000000, + Bit11 = 0b_00000000_00000000_00001000_00000000, + Bit10 = 0b_00000000_00000000_00000100_00000000, + Bit09 = 0b_00000000_00000000_00000010_00000000, + Bit08 = 0b_00000000_00000000_00000001_00000000, + Bit07 = -0b_00000000_00000000_00000000_10000000, + Bit06 = 0b_00000000_00000000_00000000_01000000, + Bit05 = 0b_00000000_00000000_00000000_00100000, + Bit04 = 0b_00000000_00000000_00000000_00010000, + Bit03 = 0b_00000000_00000000_00000000_00001000, + Bit02 = 0b_00000000_00000000_00000000_00000100, + Bit01 = 0b_00000000_00000000_00000000_00000010, + Bit00 = 0b_00000000_00000000_00000000_00000001, + All_0 = 0, + All_1 = -1 +} \ No newline at end of file diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitUInt.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitUInt.cs new file mode 100644 index 000000000..4f7d998a5 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumDefinitions/WithoutGenerator/FlagsEnum_ExplicitUInt.cs @@ -0,0 +1,43 @@ +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions.EnumDefinitions; + +/// +/// Flags enum with explicitly-defined backing type of uint and only a on the enum declaration No other attributes on the enum or its members.. +/// +[Flags] +public enum FlagsEnum_ExplicitUInt : uint +{ + Bit31 = 0b_10000000_00000000_00000000_00000000u, + Bit30 = 0b_01000000_00000000_00000000_00000000u, + Bit29 = 0b_00100000_00000000_00000000_00000000u, + Bit28 = 0b_00010000_00000000_00000000_00000000u, + Bit27 = 0b_00001000_00000000_00000000_00000000u, + Bit26 = 0b_00000100_00000000_00000000_00000000u, + Bit25 = 0b_00000010_00000000_00000000_00000000u, + Bit24 = 0b_00000001_00000000_00000000_00000000u, + Bit23 = 0b_00000000_10000000_00000000_00000000u, + Bit22 = 0b_00000000_01000000_00000000_00000000u, + Bit21 = 0b_00000000_00100000_00000000_00000000u, + Bit20 = 0b_00000000_00010000_00000000_00000000u, + Bit19 = 0b_00000000_00001000_00000000_00000000u, + Bit18 = 0b_00000000_00000100_00000000_00000000u, + Bit17 = 0b_00000000_00000010_00000000_00000000u, + Bit16 = 0b_00000000_00000001_00000000_00000000u, + Bit15 = 0b_00000000_00000000_10000000_00000000u, + Bit14 = 0b_00000000_00000000_01000000_00000000u, + Bit13 = 0b_00000000_00000000_00100000_00000000u, + Bit12 = 0b_00000000_00000000_00010000_00000000u, + Bit11 = 0b_00000000_00000000_00001000_00000000u, + Bit10 = 0b_00000000_00000000_00000100_00000000u, + Bit09 = 0b_00000000_00000000_00000010_00000000u, + Bit08 = 0b_00000000_00000000_00000001_00000000u, + Bit07 = 0b_00000000_00000000_00000000_10000000u, + Bit06 = 0b_00000000_00000000_00000000_01000000u, + Bit05 = 0b_00000000_00000000_00000000_00100000u, + Bit04 = 0b_00000000_00000000_00000000_00010000u, + Bit03 = 0b_00000000_00000000_00000000_00001000u, + Bit02 = 0b_00000000_00000000_00000000_00000100u, + Bit01 = 0b_00000000_00000000_00000000_00000010u, + Bit00 = 0b_00000000_00000000_00000000_00000001u, + All_0 = 0b_00000000_00000000_00000000_00000000u, + All_1 = 0b_11111111_11111111_11111111_11111111u +} From 7ef6c57331a350bcd7df20679f068c7893108301 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:46:52 -0700 Subject: [PATCH 14/19] Code generator tests --- ...tensionMethodsIncrementalGeneratorTests.cs | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGeneratorTests.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGeneratorTests.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGeneratorTests.cs new file mode 100644 index 000000000..29ebae6f2 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Tests/Generators/EnumExtensions/EnumExtensionMethodsIncrementalGeneratorTests.cs @@ -0,0 +1,329 @@ +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reflection; +using System.Runtime.CompilerServices; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using Terminal.Gui.Analyzers.Internal.Attributes; +using Terminal.Gui.Analyzers.Internal.Generators.EnumExtensions; + +namespace Terminal.Gui.Analyzers.Internal.Tests.Generators.EnumExtensions; + +[TestFixture] +[Category ("Source Generators")] +[TestOf (typeof (EnumExtensionMethodsIncrementalGenerator))] +[Parallelizable (ParallelScope.Children)] +public class EnumExtensionMethodsIncrementalGeneratorTests +{ + private static bool _isInitialized; + + /// All enum types declared in the test assembly. + private static readonly ObservableCollection AllEnumTypes = []; + + /// + /// All enum types without a , + /// + private static readonly HashSet BoringEnumTypes = []; + + /// All extension classes generated for enums with our attribute. + private static readonly ObservableCollection EnumExtensionClasses = []; + + private static readonly ConcurrentDictionary ExtendedEnumTypeMappings = []; + private static IEnumerable ExtendedEnumTypes => ExtendedEnumTypeMappings.Keys; + + private static readonly ReaderWriterLockSlim InitializationLock = new (); + + private static IEnumerable GetAssemblyExtendedEnumTypeAttributes () => + Assembly.GetExecutingAssembly () + .GetCustomAttributes (); + + private static IEnumerable Get_AssemblyExtendedEnumTypeAttribute_EnumHasGeneratorAttribute_Cases () + { + return GetAssemblyExtendedEnumTypeAttributes () + .Select ( + static attr => new TestCaseData (attr) + { + TestName = $"{nameof (AssemblyExtendedEnumTypeAttribute_EnumHasGeneratorAttribute)}({attr.EnumType.Name},{attr.ExtensionClass.Name})", + HasExpectedResult = true, + ExpectedResult = true + }); + } + + [Test] + [Category ("Attributes")] + [TestCaseSource (nameof (Get_AssemblyExtendedEnumTypeAttribute_EnumHasGeneratorAttribute_Cases))] + public bool AssemblyExtendedEnumTypeAttribute_EnumHasGeneratorAttribute (AssemblyExtendedEnumTypeAttribute attr) + { + Assume.That (attr, Is.Not.Null); + Assume.That (attr.EnumType, Is.Not.Null); + Assume.That (attr.EnumType!.IsEnum); + + return attr.EnumType.IsDefined (typeof (GenerateEnumExtensionMethodsAttribute)); + } + + private const string AssemblyExtendedEnumTypeAttributeEnumPropertyName = + $"{nameof (AssemblyExtendedEnumTypeAttribute)}.{nameof (AssemblyExtendedEnumTypeAttribute.EnumType)}"; + + [Test] + [Category("Attributes")] + public void AssemblyExtendedEnumTypeAttribute_ExtensionClassHasExpectedReverseMappingAttribute ([ValueSource(nameof(GetAssemblyExtendedEnumTypeAttributes))]AssemblyExtendedEnumTypeAttribute attr) + { + Assume.That (attr, Is.Not.Null); + Assume.That (attr.ExtensionClass, Is.Not.Null); + Assume.That (attr.ExtensionClass!.IsClass); + Assume.That (attr.ExtensionClass!.IsSealed); + + Assert.That (attr.ExtensionClass.IsDefined (typeof (ExtensionsForEnumTypeAttribute<>))); + } + + [Test] + [Category("Attributes")] + public void ExtendedEnum_AssemblyHasMatchingAttribute ([ValueSource(nameof(GetExtendedEnum_EnumData))]EnumData enumData) + { + Assume.That (enumData, Is.Not.Null); + Assume.That (enumData.EnumType, Is.Not.Null); + Assume.That (enumData.EnumType!.IsEnum); + + Assert.That (enumData.EnumType, Has.Attribute ()); + } + + [Test] + public void BoringEnum_DoesNotHaveExtensions ([ValueSource (nameof (BoringEnumTypes))] Type enumType) + { + Assume.That (enumType.IsEnum); + + Assert.That (enumType, Has.No.Attribute ()); + } + + [Test] + public void ExtendedEnum_FastIsDefinedFalse_DoesNotHaveFastIsDefined ([ValueSource (nameof (GetExtendedEnumTypes_FastIsDefinedFalse))] EnumData enumData) + { + Assume.That (enumData.EnumType.IsEnum); + Assume.That (enumData.EnumType, Has.Attribute ()); + Assume.That (enumData.GeneratorAttribute, Is.Not.Null); + Assume.That (enumData.GeneratorAttribute, Is.EqualTo (enumData.EnumType.GetCustomAttribute ())); + Assume.That (enumData.GeneratorAttribute, Has.Property ("FastIsDefined").False); + Assume.That (enumData.ExtensionClass, Is.Not.Null); + + Assert.That (enumData.ExtensionClass!.GetMethod ("FastIsDefined"), Is.Null); + } + + [Test] + public void ExtendedEnum_StaticExtensionClassExists ([ValueSource (nameof (ExtendedEnumTypes))] Type enumType) + { + Assume.That (enumType.IsEnum); + Assume.That (enumType, Has.Attribute ()); + ITypeInfo enumTypeInfo = new TypeWrapper (enumType); + Assume.That (enumType, Has.Attribute ()); + } + + [Test] + public void ExtendedEnum_FastIsDefinedTrue_HasFastIsDefined ([ValueSource (nameof (GetExtendedEnumTypes_FastIsDefinedTrue))] EnumData enumData) + { + Assume.That (enumData.EnumType, Is.Not.Null); + Assume.That (enumData.EnumType.IsEnum); + Assume.That (enumData.EnumType, Has.Attribute ()); + Assume.That (enumData.ExtensionClass, Is.Not.Null); + ITypeInfo extensionClassTypeInfo = new TypeWrapper (enumData.ExtensionClass!); + Assume.That (extensionClassTypeInfo.IsStaticClass); + Assume.That (enumData.GeneratorAttribute, Is.Not.Null); + Assume.That (enumData.GeneratorAttribute, Is.EqualTo (enumData.EnumType.GetCustomAttribute ())); + Assume.That (enumData.GeneratorAttribute, Has.Property ("FastIsDefined").True); + + MethodInfo? fastIsDefinedMethod = enumData.ExtensionClass!.GetMethod ("FastIsDefined"); + + Assert.That (fastIsDefinedMethod, Is.Not.Null); + Assert.That (fastIsDefinedMethod, Has.Attribute ()); + IMethodInfo[] extensionMethods = extensionClassTypeInfo.GetMethodsWithAttribute (false); + + + } + + private static IEnumerable GetExtendedEnum_EnumData () + { + InitializationLock.EnterUpgradeableReadLock (); + + try + { + if (!_isInitialized) + { + Initialize (); + } + + return ExtendedEnumTypeMappings.Values; + } + finally + { + InitializationLock.ExitUpgradeableReadLock (); + } + } + + private static IEnumerable GetBoringEnumTypes () + { + InitializationLock.EnterUpgradeableReadLock (); + + try + { + if (!_isInitialized) + { + Initialize (); + } + + return BoringEnumTypes; + } + finally + { + InitializationLock.ExitUpgradeableReadLock (); + } + } + + private static IEnumerable GetExtendedEnumTypes_FastIsDefinedFalse () + { + InitializationLock.EnterUpgradeableReadLock (); + + try + { + if (!_isInitialized) + { + Initialize (); + } + + return ExtendedEnumTypeMappings.Values.Where (static t => t.GeneratorAttribute?.FastIsDefined is false); + } + finally + { + InitializationLock.ExitUpgradeableReadLock (); + } + } + + private static IEnumerable GetExtendedEnumTypes_FastIsDefinedTrue () + { + InitializationLock.EnterUpgradeableReadLock (); + + try + { + if (!_isInitialized) + { + Initialize (); + } + + return ExtendedEnumTypeMappings.Values.Where (static t => t.GeneratorAttribute?.FastIsDefined is true); + } + finally + { + InitializationLock.ExitUpgradeableReadLock (); + } + } + + private static void Initialize () + { + if (!InitializationLock.IsUpgradeableReadLockHeld || !InitializationLock.TryEnterWriteLock (5000)) + { + return; + } + + try + { + if (_isInitialized) + { + return; + } + + AllEnumTypes.CollectionChanged += AllEnumTypes_CollectionChanged; + EnumExtensionClasses.CollectionChanged += EnumExtensionClasses_OnCollectionChanged; + + Type [] allAssemblyTypes = Assembly + .GetExecutingAssembly () + .GetTypes (); + + IEnumerable allEnumTypes = allAssemblyTypes.Where (IsDefinedEnum); + + foreach (Type type in allEnumTypes) + { + AllEnumTypes.Add (type); + } + + foreach (Type type in allAssemblyTypes.Where (static t => t.IsClass && t.IsDefined (typeof (ExtensionsForEnumTypeAttribute<>)))) + { + EnumExtensionClasses.Add (type); + } + + _isInitialized = true; + } + finally + { + InitializationLock.ExitWriteLock (); + } + + return; + + static bool IsDefinedEnum (Type t) { return t is { IsEnum: true, IsGenericType: false, IsConstructedGenericType: false, IsTypeDefinition: true }; } + + static void AllEnumTypes_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action is not NotifyCollectionChangedAction.Add and not NotifyCollectionChangedAction.Replace || e.NewItems is null) + { + return; + } + + foreach (Type enumType in e.NewItems.OfType ()) + { + if (enumType.GetCustomAttribute () is not { } generatorAttribute) + { + BoringEnumTypes.Add (enumType); + + continue; + } + + ExtendedEnumTypeMappings.AddOrUpdate ( + enumType, + CreateNewEnumData, + UpdateGeneratorAttributeProperty, + generatorAttribute); + } + } + + static EnumData CreateNewEnumData (Type tEnum, GenerateEnumExtensionMethodsAttribute attr) { return new (tEnum, attr); } + + static EnumData UpdateGeneratorAttributeProperty (Type tEnum, EnumData data, GenerateEnumExtensionMethodsAttribute attr) + { + data.GeneratorAttribute ??= attr; + + return data; + } + + static void EnumExtensionClasses_OnCollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Add) + { + return; + } + + foreach (Type extensionClassType in e.NewItems!.OfType ()) + { + if (extensionClassType.GetCustomAttribute (typeof (ExtensionsForEnumTypeAttribute<>), false) is not IExtensionsForEnumTypeAttributes + { + EnumType.IsEnum: true + } extensionForAttribute) + { + continue; + } + + ExtendedEnumTypeMappings [extensionForAttribute.EnumType].ExtensionClass ??= extensionClassType; + } + } + } + + public sealed record EnumData ( + Type EnumType, + GenerateEnumExtensionMethodsAttribute? GeneratorAttribute = null, + Type? ExtensionClass = null, + IExtensionsForEnumTypeAttributes? ExtensionForEnumTypeAttribute = null) + { + public Type? ExtensionClass { get; set; } = ExtensionClass; + + public IExtensionsForEnumTypeAttributes? ExtensionForEnumTypeAttribute { get; set; } = ExtensionForEnumTypeAttribute; + public GenerateEnumExtensionMethodsAttribute? GeneratorAttribute { get; set; } = GeneratorAttribute; + } +} From cdd2cc7e10e39904546da4c365d292c3e9aa19fb Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 16:47:18 -0700 Subject: [PATCH 15/19] Skeleton program.cs for basic debugging of analyzers --- .../Program.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Program.cs diff --git a/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Program.cs b/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Program.cs new file mode 100644 index 000000000..4d1e4a5c3 --- /dev/null +++ b/Analyzers/Terminal.Gui.Analyzers.Internal.Debugging/Program.cs @@ -0,0 +1,21 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui.Analyzers.Internal.Debugging; + +class Program +{ + static void Main (string [] args) + { + + } +} + +[GenerateEnumExtensionMethods] +public enum TestEnum +{ + Zero = 0, + One, + Two = 2, + Three, + Six = 6 +} \ No newline at end of file From 683c0875377749accfe6561ff1e79e4752fb352a Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 19 Apr 2024 23:18:16 -0700 Subject: [PATCH 16/19] Add manifest for powershell module --- Scripts/Terminal.Gui.PowerShell.psd1 | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 Scripts/Terminal.Gui.PowerShell.psd1 diff --git a/Scripts/Terminal.Gui.PowerShell.psd1 b/Scripts/Terminal.Gui.PowerShell.psd1 new file mode 100644 index 000000000..c6e48703e --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.psd1 @@ -0,0 +1,123 @@ +# +# Module manifest for module 'Terminal.Gui.Powershell' +# +# Generated by: Brandon Thetford (GitHub @dodexahedron) +# +# Generated on: 4/19/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Terminal.Gui.PowerShell.psm1' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = 'f28198f9-cf4b-4ab0-9f94-aef5616b7989' + +# Author of this module +Author = 'Brandon Thetford (GitHub @dodexahedron)' + +# Company or vendor of this module +CompanyName = 'The Terminal.Gui Project' + +# Copyright statement for this module +Copyright = '(c) Brandon Thetford (GitHub @dodexahedron), provided under the MIT license' + +# Description of the functionality provided by this module +Description = 'Commands for operations on components of Terminal.Gui during development of Terminal.Gui' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +ProcessorArchitecture = 'None' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @('Microsoft.PowerShell.Utility','Microsoft.PowerShell.Management','PSReadLine') + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @('Build-Analyzers','Close-Solution','Open-Solution') + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +# VariablesToExport = () + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +FileList = 'Terminal.Gui.PowerShell.psm1' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/Scripts/COPYRIGHT' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/gui-cs/Terminal.Gui' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + From b9b68ba1a65b142f93c22ad40de19ee796160b0a Mon Sep 17 00:00:00 2001 From: dodexahedron Date: Thu, 25 Apr 2024 13:39:01 -0700 Subject: [PATCH 17/19] Re-structured and formalized the scripts and modules. --- Scripts/ConfigureEnvironment.ps1 | 11 + Scripts/Load-Module.ps1 | 10 + Scripts/ResetEnvironment.ps1 | 32 +++ ...art-Terminal.GUI.PowerShellEnvironment.ps1 | 8 + .../Terminal.Gui.PowerShell.Analyzers.psm1 | 105 +++++++++ Scripts/Terminal.Gui.PowerShell.psd1 | 39 ++-- Scripts/Terminal.Gui.PowerShell.psm1 | 212 +++++++++++------- .../Terminal.Gui.Powershell.Analyzers.psd1 | 126 +++++++++++ 8 files changed, 440 insertions(+), 103 deletions(-) create mode 100644 Scripts/ConfigureEnvironment.ps1 create mode 100644 Scripts/Load-Module.ps1 create mode 100644 Scripts/ResetEnvironment.ps1 create mode 100644 Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 create mode 100644 Scripts/Terminal.Gui.PowerShell.Analyzers.psm1 create mode 100644 Scripts/Terminal.Gui.Powershell.Analyzers.psd1 diff --git a/Scripts/ConfigureEnvironment.ps1 b/Scripts/ConfigureEnvironment.ps1 new file mode 100644 index 000000000..97511bd9b --- /dev/null +++ b/Scripts/ConfigureEnvironment.ps1 @@ -0,0 +1,11 @@ +<# + .SYNOPSIS + Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. + .DESCRIPTION + Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. + Also modifies the prompt to indicate the session has been altered. + Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. +#> + + +Set-Environment \ No newline at end of file diff --git a/Scripts/Load-Module.ps1 b/Scripts/Load-Module.ps1 new file mode 100644 index 000000000..f8c9c90b3 --- /dev/null +++ b/Scripts/Load-Module.ps1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + Loads the Terminal.Gui.PowerShell modules and pushes the current path to the location stack. +#> + + +$tgScriptsPath = Push-Location -PassThru +$tgModule = Import-Module "./Terminal.Gui.PowerShell.psd1" -PassThru + +Set-PowerShellEnvironment \ No newline at end of file diff --git a/Scripts/ResetEnvironment.ps1 b/Scripts/ResetEnvironment.ps1 new file mode 100644 index 000000000..c21f8048f --- /dev/null +++ b/Scripts/ResetEnvironment.ps1 @@ -0,0 +1,32 @@ +<# + .SYNOPSIS + Resets changes made by ConfigureEnvironment.pst to the current PowerShell environment. + .DESCRIPTION + Optional script to undo changes to the current session made by ConfigureEnvironment.ps1. + Changes only affect the current session, so exiting will also "reset." + .PARAMETER Exit + Switch parameter that, if specified, exits the current PowerShell environment. + Does not bother doing any other operations, as none are necessary. + .INPUTS + None + .OUTPUTS + None + .EXAMPLE + .\ResetEnvironment.ps1 + To run the script to undo changes in the current session. + .EXAMPLE + .\ResetEnvironment.ps1 -Exit + To exit the current session. Same as simply using the Exit command. +#> + + +# The two blank lines above must be preserved. +Import-Module ./Terminal.Gui.PowerShell.psd1 + +if($args -contains "-Exit"){ + [Environment]::Exit(0) +} else { + Reset-PowerShellEnvironment +} + +Remove-Module Terminal.Gui.PowerShell \ No newline at end of file diff --git a/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 b/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 new file mode 100644 index 000000000..cc9490b3b --- /dev/null +++ b/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 @@ -0,0 +1,8 @@ +<# + .SYNOPSIS + Start a Terminal.Gui.PowerShell environment and load all modules. + .DESCRIPTION + Starts a new Terminal.Gui.PowerShell environment, with all modules imported. +#> + +. ./Load-Module.ps1 \ No newline at end of file diff --git a/Scripts/Terminal.Gui.PowerShell.Analyzers.psm1 b/Scripts/Terminal.Gui.PowerShell.Analyzers.psm1 new file mode 100644 index 000000000..78a87a448 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Analyzers.psm1 @@ -0,0 +1,105 @@ +<# + .SYNOPSIS + Builds all analyzer projects in Debug and Release configurations. + .DESCRIPTION + Uses dotnet build to build all analyzer projects, with optional behavior changes via switch parameters. + .PARAMETER AutoClose + Automatically close running Visual Studio processes which have the Terminal.sln solution loaded, before taking any other actions. + .PARAMETER AutoLaunch + Automatically start a new Visual Studio process and load the solution after completion. + .PARAMETER Force + Carry out operations unconditionally and do not prompt for confirmation. + .PARAMETER NoClean + Do not delete the bin and obj folders before building the analyzers. Usually best not to use this, but can speed up the builds slightly. + .PARAMETER Quiet + Write less text output to the terminal. + .INPUTS + None + .OUTPUTS + None +#> +Function Build-Analyzers { + [CmdletBinding()] + param( + [Parameter(Mandatory=$false, HelpMessage="Automatically close running Visual Studio processes which have the Terminal.sln solution loaded, before taking any other actions.")] + [switch]$AutoClose, + [Parameter(Mandatory=$false, HelpMessage="Automatically start a new Visual Studio process and load the solution after completion.")] + [switch]$AutoLaunch, + [Parameter(Mandatory=$false, HelpMessage="Carry out operations unconditionally and do not prompt for confirmation.")] + [switch]$Force, + [Parameter(Mandatory=$false, HelpMessage="Do not delete the bin and obj folders before building the analyzers.")] + [switch]$NoClean, + [Parameter(Mandatory=$false, HelpMessage="Write less text output to the terminal.")] + [switch]$Quiet + ) + + if($AutoClose) { + if(!$Quiet) { + Write-Host Closing Visual Studio processes + } + Close-Solution + } + + if($Force){ + $response = 'Y' + } + elseif(!$Force && $NoClean){ + $response = ($r = Read-Host "Pre-build Terminal.Gui.InternalAnalyzers without removing old build artifacts? [Y/n]") ? $r : 'Y' + } + else{ + $response = ($r = Read-Host "Delete bin and obj folders for Terminal.Gui and Terminal.Gui.InternalAnalyzers and pre-build Terminal.Gui.InternalAnalyzers? [Y/n]") ? $r : 'Y' + } + + if (($response -ne 'Y')) { + Write-Host Took no action + return + } + + New-Variable -Name solutionRoot -Visibility Public -Value (Resolve-Path ..) + Push-Location $solutionRoot + New-Variable -Name solutionFile -Visibility Public -Value (Resolve-Path ./Terminal.sln) + $mainProjectRoot = Resolve-Path ./Terminal.Gui + $mainProjectFile = Join-Path $mainProjectRoot Terminal.Gui.csproj + $analyzersRoot = Resolve-Path ./Analyzers + $internalAnalyzersProjectRoot = Join-Path $analyzersRoot Terminal.Gui.Analyzers.Internal + $internalAnalyzersProjectFile = Join-Path $internalAnalyzersProjectRoot Terminal.Gui.Analyzers.Internal.csproj + + if(!$NoClean) { + if(!$Quiet) { + Write-Host Deleting bin and obj folders for Terminal.Gui + } + if(Test-Path $mainProjectRoot/bin) { + Remove-Item -Recurse -Force $mainProjectRoot/bin + Remove-Item -Recurse -Force $mainProjectRoot/obj + } + + if(!$Quiet) { + Write-Host Deleting bin and obj folders for Terminal.Gui.InternalAnalyzers + } + if(Test-Path $internalAnalyzersProjectRoot/bin) { + Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/bin + Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/obj + } + } + + if(!$Quiet) { + Write-Host Building analyzers in Debug configuration + } + dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Debug + + if(!$Quiet) { + Write-Host Building analyzers in Release configuration + } + dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Release + + if(!$AutoLaunch) { + Write-Host -ForegroundColor Green Finished. Restart Visual Studio for changes to take effect. + } else { + if(!$Quiet) { + Write-Host -ForegroundColor Green Finished. Re-loading Terminal.sln. + } + Open-Solution + } + + return +} \ No newline at end of file diff --git a/Scripts/Terminal.Gui.PowerShell.psd1 b/Scripts/Terminal.Gui.PowerShell.psd1 index c6e48703e..c496477dd 100644 --- a/Scripts/Terminal.Gui.PowerShell.psd1 +++ b/Scripts/Terminal.Gui.PowerShell.psd1 @@ -1,5 +1,5 @@ # -# Module manifest for module 'Terminal.Gui.Powershell' +# Module manifest for module 'Terminal.Gui.PowerShell' # # Generated by: Brandon Thetford (GitHub @dodexahedron) # @@ -15,7 +15,7 @@ RootModule = 'Terminal.Gui.PowerShell.psm1' ModuleVersion = '1.0.0' # Supported PSEditions -# CompatiblePSEditions = @() +CompatiblePSEditions = @('Core') # ID used to uniquely identify this module GUID = 'f28198f9-cf4b-4ab0-9f94-aef5616b7989' @@ -27,19 +27,24 @@ Author = 'Brandon Thetford (GitHub @dodexahedron)' CompanyName = 'The Terminal.Gui Project' # Copyright statement for this module -Copyright = '(c) Brandon Thetford (GitHub @dodexahedron), provided under the MIT license' +Copyright = 'Brandon Thetford (GitHub @dodexahedron), provided to the Terminal.Gui project and you under the MIT license' # Description of the functionality provided by this module -Description = 'Commands for operations on components of Terminal.Gui during development of Terminal.Gui' +Description = 'Utilities for development-time operations on and management of components of Terminal.Gui code and other assets.' # Minimum version of the PowerShell engine required by this module -# PowerShellVersion = '' +PowerShellVersion = '7.4.0' + +# Name of the PowerShell "host" subsystem (not system host name). Helps ensure that we know what to expect from the environment. +PowerShellHostName = 'ConsoleHost' # Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' +PowerShellHostVersion = '7.4.0' -# Processor architecture (None, X86, Amd64) required by this module -ProcessorArchitecture = 'None' +# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only. +# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else. +# Has nothing to do with runtime use of Terminal.Gui. +ProcessorArchitecture = 'AMD64' # Modules that must be imported into the global environment prior to importing this module RequiredModules = @('Microsoft.PowerShell.Utility','Microsoft.PowerShell.Management','PSReadLine') @@ -57,28 +62,26 @@ RequiredModules = @('Microsoft.PowerShell.Utility','Microsoft.PowerShell.Managem # FormatsToProcess = @() # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() +NestedModules = @('./Terminal.Gui.PowerShell.Analyzers.psd1') # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @('Build-Analyzers','Close-Solution','Open-Solution') +FunctionsToExport = @('Build-Analyzers','Close-Solution','Open-Solution','Reset-PowerShellEnvironment','Set-PowerShellEnvironment') +#FunctionsToExport = @('*') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() # Variables to export from this module -# VariablesToExport = () +VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @() -# DSC resources to export from this module -# DscResourcesToExport = @() - # List of all modules packaged with this module # ModuleList = @() # List of all files packaged with this module -FileList = 'Terminal.Gui.PowerShell.psm1' +# FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ @@ -89,7 +92,7 @@ PrivateData = @{ # Tags = @() # A URL to the license for this module. - LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/Scripts/COPYRIGHT' + LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/tree/v2_develop/Scripts/COPYRIGHT' # A URL to the main website for this project. ProjectUri = 'https://github.com/gui-cs/Terminal.Gui' @@ -98,13 +101,13 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - # ReleaseNotes = '' + ReleaseNotes = 'See change history and releases for Terminal.Gui on GitHub' # Prerelease string of this module # Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + RequireLicenseAcceptance = $false # External dependent modules of this module # ExternalModuleDependencies = @() diff --git a/Scripts/Terminal.Gui.PowerShell.psm1 b/Scripts/Terminal.Gui.PowerShell.psm1 index 6e619ba20..ec435a6f7 100644 --- a/Scripts/Terminal.Gui.PowerShell.psm1 +++ b/Scripts/Terminal.Gui.PowerShell.psm1 @@ -1,100 +1,142 @@ -Function Build-Analyzers { +<# + .SYNOPSIS + (Windows Only) Opens Visual Studio and loads Terminal.sln. + .DESCRIPTION + (Windows Only) Opens Visual Studio and loads Terminal.sln. + .PARAMETER SolutionFilePath + (Optional) If specified, the path to the solution file. Typically unnecessary to supply this parameter. + .INPUTS + None + .OUTPUTS + None +#> +Function Open-Solution { [CmdletBinding()] param( - [Parameter(Mandatory=$false, HelpMessage="Automatically close running Visual Studio processes which have the Terminal.sln solution loaded, before taking any other actions.")] - [switch]$AutoClose, - [Parameter(Mandatory=$false, HelpMessage="Automatically start a new Visual Studio process and load the solution after completion.")] - [switch]$AutoLaunch, - [Parameter(Mandatory=$false, HelpMessage="Carry out operations unconditionally and do not prompt for confirmation.")] - [switch]$Force, - [Parameter(Mandatory=$false, HelpMessage="Do not delete the bin and obj folders before building the analyzers.")] - [switch]$NoClean, - [Parameter(Mandatory=$false, HelpMessage="Write less text output to the terminal.")] - [switch]$Quiet + [Parameter(Mandatory=$false, HelpMessage="The path to the solution file to open.")] + [Uri]$SolutionFilePath ) - if($AutoClose) { - if(!$Quiet) { - Write-Host Closing Visual Studio processes - } - Close-Solution - } - - if($Force){ - $response = 'Y' - } - elseif(!$Force && $NoClean){ - $response = ($r = Read-Host "Pre-build Terminal.Gui.InternalAnalyzers without removing old build artifacts? [Y/n]") ? $r : 'Y' - } - else{ - $response = ($r = Read-Host "Delete bin and obj folders for Terminal.Gui and Terminal.Gui.InternalAnalyzers and pre-build Terminal.Gui.InternalAnalyzers? [Y/n]") ? $r : 'Y' - } - - if (($response -ne 'Y')) { - Write-Host Took no action - return + if(!$IsWindows) { + [string]$warningMessage = "The Open-Solution cmdlet is only supported on Windows.`n` + Attempt to open file $SolutionFilePath with the system default handler?" + + Write-Warning $warningMessage -WarningAction Inquire } - New-Variable -Name solutionRoot -Visibility Public -Value (Resolve-Path ..) - Push-Location $solutionRoot - New-Variable -Name solutionFile -Visibility Public -Value (Resolve-Path ./Terminal.sln) - $mainProjectRoot = Resolve-Path ./Terminal.Gui - $mainProjectFile = Join-Path $mainProjectRoot Terminal.Gui.csproj - $analyzersRoot = Resolve-Path ./Analyzers - $internalAnalyzersProjectRoot = Join-Path $analyzersRoot Terminal.Gui.Analyzers.Internal - $internalAnalyzersProjectFile = Join-Path $internalAnalyzersProjectRoot Terminal.Gui.Analyzers.Internal.csproj - - if(!$NoClean) { - if(!$Quiet) { - Write-Host Deleting bin and obj folders for Terminal.Gui - } - if(Test-Path $mainProjectRoot/bin) { - Remove-Item -Recurse -Force $mainProjectRoot/bin - Remove-Item -Recurse -Force $mainProjectRoot/obj - } - - if(!$Quiet) { - Write-Host Deleting bin and obj folders for Terminal.Gui.InternalAnalyzers - } - if(Test-Path $internalAnalyzersProjectRoot/bin) { - Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/bin - Remove-Item -Recurse -Force $internalAnalyzersProjectRoot/obj - } - } - - if(!$Quiet) { - Write-Host Building analyzers in Debug configuration - } - dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Debug - - if(!$Quiet) { - Write-Host Building analyzers in Release configuration - } - dotnet build $internalAnalyzersProjectFile --no-incremental --nologo --force --configuration Release - - if(!$AutoLaunch) { - Write-Host -ForegroundColor Green Finished. Restart Visual Studio for changes to take effect. - } else { - if(!$Quiet) { - Write-Host -ForegroundColor Green Finished. Re-loading Terminal.sln. - } - Open-Solution - } - + Invoke-Item $SolutionFilePath return } -Function Open-Solution { - Invoke-Item $solutionFile - return -} - +<# + .SYNOPSIS + (Windows Only) Closes Visual Studio processes with Terminal.sln loaded. + .DESCRIPTION + (Windows Only) Closes Visual Studio processes with Terminal.sln loaded by finding any VS processes launched with the solution file or with 'Terminal' in their main window titles. + .INPUTS + None + .OUTPUTS + None +#> Function Close-Solution { $vsProcesses = Get-Process -Name devenv | Where-Object { ($_.CommandLine -Match ".*Terminal\.sln.*" -or $_.MainWindowTitle -Match "Terminal.*") } Stop-Process -InputObject $vsProcesses Remove-Variable vsProcesses } -Export-ModuleMember -Function Build-Analyzers -Export-ModuleMember -Function Open-Solution -Export-ModuleMember -Function Close-Solution +<# + .SYNOPSIS + Resets changes made by ConfigureEnvironment.pst to the current PowerShell environment. + .DESCRIPTION + Optional function to undo changes to the current session made by ConfigureEnvironment.ps1. + Changes only affect the current session, so exiting will also "reset." + .PARAMETER Exit + Switch parameter that, if specified, exits the current PowerShell environment. + Does not bother doing any other operations, as none are necessary. + .INPUTS + None + .OUTPUTS + None + .EXAMPLE + Reset-PowerShellEnvironment + To undo changes in the current session. + .EXAMPLE + Reset-PowerShellEnvironment -Exit + To exit the current session. Same as simply using the Exit command. +#> +Function Reset-PowerShellEnvironment { + param( + [Parameter(Mandatory = $false)] + [switch]$Exit + ) + + if($Exit) { + [Environment]::Exit(0) + } + + if(Get-Variable -Name NormalPrompt -Scope Global -ErrorAction SilentlyContinue){ + Set-Item Function:prompt $NormalPrompt + Remove-Variable -Name NormalPrompt -Scope Global -Force + } + + if(Get-Variable -Name OriginalPSModulePath -Scope Global -ErrorAction SilentlyContinue){ + $Env:PSModulePath = $OriginalPSModulePath + Remove-Variable -Name OriginalPSModulePath -Scope Global -Force + } + + Remove-Variable -Name PathVarSeparator -Scope Global -Force -ErrorAction SilentlyContinue +} + +<# + .SYNOPSIS + Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. + .DESCRIPTION + Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. + Also modifies the prompt to indicate the session has been altered. + Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. +#> + +<# + .SYNOPSIS + Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. + .DESCRIPTION + Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. + Also modifies the prompt to indicate the session has been altered. + Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. +#> +Function Set-PowerShellEnvironment { + # Set a custom prompt to indicate we're in our modified environment. + # Save the normal one first, though. + New-Variable -Name NormalPrompt -Option ReadOnly -Scope Global -Value (Get-Item Function:prompt).ScriptBlock -ErrorAction SilentlyContinue + Set-Item Function:prompt { "TGPS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "; } + + # Save existing PSModulePath for optional reset later. + # If it is already saved, do not overwrite, but continue anyway. + New-Variable -Name OriginalPSModulePath -Visibility Public -Option ReadOnly -Scope Global -Value ($Env:PSModulePath) -ErrorAction SilentlyContinue + Write-Debug -Message "`$OriginalPSModulePath is $OriginalPSModulePath" + + # Get platform-specific path variable entry separator. Continue if it's already set. + New-Variable -Name PathVarSeparator -Visibility Public -Option ReadOnly,Constant -Scope Global -Value ";" -Description 'Separator character used in environment variables such as $Env:PSModulePath' -ErrorAction SilentlyContinue + + if(!$IsWindows) { + $PathVarSeparator = ':' + } + Write-Debug -Message "`$PathVarSeparator is $PathVarSeparator" + + # Now make it constant. + Set-Variable PathVarSeparator -Option Constant -ErrorAction SilentlyContinue + + # If Env:PSModulePath already has the current path, don't append it again. + if($Env:PSModulePath -notlike "*$((Resolve-Path .).Path)*") { + Write-Debug -Message "Appending $((Resolve-Path .).Path) to `$Env:PSModulePath" + $env:PSModulePath = Join-String -Separator $PathVarSeparator -InputObject @( $env:PSModulePath, (Resolve-Path .).Path ) + } + Write-Debug -Message "`$Env:PSModulePath is $Env:PSModulePath" +} + +# This ensures the environment is reset when unloading the module. +# Without this, function:prompt will be undefined. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + Reset-PowerShellEnvironment + Pop-Location +} \ No newline at end of file diff --git a/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 b/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 new file mode 100644 index 000000000..c8477105d --- /dev/null +++ b/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 @@ -0,0 +1,126 @@ +# +# Module manifest for module 'Terminal.Gui.Powershell.Analyzers' +# +# Generated by: Brandon Thetford (GitHub @dodexahedron) +# +# Generated on: 4/24/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Terminal.Gui.PowerShell.Analyzers.psm1' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +CompatiblePSEditions = @('Core') + +# ID used to uniquely identify this module +GUID = '3e85001d-6539-4cf1-b71c-ec9e983f7fc8' + +# Author of this module +Author = 'Brandon Thetford (GitHub @dodexahedron)' + +# Company or vendor of this module +CompanyName = 'The Terminal.Gui Project' + +# Copyright statement for this module +Copyright = '(c) Brandon Thetford (GitHub @dodexahedron). Provided to the Terminal.Gui project and you under the terms of the MIT License.' + +# Description of the functionality provided by this module +Description = 'Operations involving Terminal.Gui analyzer projects, fur use during development of Terminal.Gui' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.4.0' + +# Name of the PowerShell host required by this module +PowerShellHostName = 'ConsoleHost' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +ProcessorArchitecture = 'Amd64' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Utility') + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @('Build-Analyzers') + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +FileList = './Terminal.Gui.Powershell.Analyzers.psm1' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/Scripts/COPYRIGHT' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/gui-cs/Terminal.Gui' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + From 8cb748e45274382dcf186cd1320f0dc0e43e30d4 Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 26 Apr 2024 21:23:04 -0700 Subject: [PATCH 18/19] Reorganized, simplified, and standardized PowerShell modules. --- Scripts/ConfigureEnvironment.ps1 | 11 -- Scripts/Load-Module.ps1 | 10 -- Scripts/ResetEnvironment.ps1 | 32 ---- ...art-Terminal.GUI.PowerShellEnvironment.ps1 | 8 - Scripts/Terminal.Gui.PowerShell.Build.psd1 | 131 +++++++++++++++++ Scripts/Terminal.Gui.PowerShell.Build.psm1 | 32 ++++ Scripts/Terminal.Gui.PowerShell.Core.psd1 | 138 ++++++++++++++++++ ...psm1 => Terminal.Gui.PowerShell.Core.psm1} | 107 +++++++------- Scripts/Terminal.Gui.PowerShell.Git.psd1 | 135 +++++++++++++++++ Scripts/Terminal.Gui.PowerShell.Git.psm1 | 111 ++++++++++++++ Scripts/Terminal.Gui.PowerShell.psd1 | 62 +++++--- .../Terminal.Gui.Powershell.Analyzers.psd1 | 21 +-- 12 files changed, 650 insertions(+), 148 deletions(-) delete mode 100644 Scripts/ConfigureEnvironment.ps1 delete mode 100644 Scripts/Load-Module.ps1 delete mode 100644 Scripts/ResetEnvironment.ps1 delete mode 100644 Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 create mode 100644 Scripts/Terminal.Gui.PowerShell.Build.psd1 create mode 100644 Scripts/Terminal.Gui.PowerShell.Build.psm1 create mode 100644 Scripts/Terminal.Gui.PowerShell.Core.psd1 rename Scripts/{Terminal.Gui.PowerShell.psm1 => Terminal.Gui.PowerShell.Core.psm1} (77%) create mode 100644 Scripts/Terminal.Gui.PowerShell.Git.psd1 create mode 100644 Scripts/Terminal.Gui.PowerShell.Git.psm1 diff --git a/Scripts/ConfigureEnvironment.ps1 b/Scripts/ConfigureEnvironment.ps1 deleted file mode 100644 index 97511bd9b..000000000 --- a/Scripts/ConfigureEnvironment.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -<# - .SYNOPSIS - Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. - .DESCRIPTION - Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. - Also modifies the prompt to indicate the session has been altered. - Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. -#> - - -Set-Environment \ No newline at end of file diff --git a/Scripts/Load-Module.ps1 b/Scripts/Load-Module.ps1 deleted file mode 100644 index f8c9c90b3..000000000 --- a/Scripts/Load-Module.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -<# - .SYNOPSIS - Loads the Terminal.Gui.PowerShell modules and pushes the current path to the location stack. -#> - - -$tgScriptsPath = Push-Location -PassThru -$tgModule = Import-Module "./Terminal.Gui.PowerShell.psd1" -PassThru - -Set-PowerShellEnvironment \ No newline at end of file diff --git a/Scripts/ResetEnvironment.ps1 b/Scripts/ResetEnvironment.ps1 deleted file mode 100644 index c21f8048f..000000000 --- a/Scripts/ResetEnvironment.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -<# - .SYNOPSIS - Resets changes made by ConfigureEnvironment.pst to the current PowerShell environment. - .DESCRIPTION - Optional script to undo changes to the current session made by ConfigureEnvironment.ps1. - Changes only affect the current session, so exiting will also "reset." - .PARAMETER Exit - Switch parameter that, if specified, exits the current PowerShell environment. - Does not bother doing any other operations, as none are necessary. - .INPUTS - None - .OUTPUTS - None - .EXAMPLE - .\ResetEnvironment.ps1 - To run the script to undo changes in the current session. - .EXAMPLE - .\ResetEnvironment.ps1 -Exit - To exit the current session. Same as simply using the Exit command. -#> - - -# The two blank lines above must be preserved. -Import-Module ./Terminal.Gui.PowerShell.psd1 - -if($args -contains "-Exit"){ - [Environment]::Exit(0) -} else { - Reset-PowerShellEnvironment -} - -Remove-Module Terminal.Gui.PowerShell \ No newline at end of file diff --git a/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 b/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 deleted file mode 100644 index cc9490b3b..000000000 --- a/Scripts/Start-Terminal.GUI.PowerShellEnvironment.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -<# - .SYNOPSIS - Start a Terminal.Gui.PowerShell environment and load all modules. - .DESCRIPTION - Starts a new Terminal.Gui.PowerShell environment, with all modules imported. -#> - -. ./Load-Module.ps1 \ No newline at end of file diff --git a/Scripts/Terminal.Gui.PowerShell.Build.psd1 b/Scripts/Terminal.Gui.PowerShell.Build.psd1 new file mode 100644 index 000000000..9f367487a --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Build.psd1 @@ -0,0 +1,131 @@ +@{ + +# No root module because this is a manifest module. +RootModule = '' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +CompatiblePSEditions = @('Core') + +# ID used to uniquely identify this module +GUID = 'c4a1de77-83fb-45a3-b1b5-18d275ef3601' + +# Author of this module +Author = 'Brandon Thetford (GitHub @dodexahedron)' + +# Company or vendor of this module +CompanyName = 'The Terminal.Gui Project' + +# Copyright statement for this module +Copyright = 'Brandon Thetford (GitHub @dodexahedron), provided to the Terminal.Gui project and you under the MIT license' + +# Description of the functionality provided by this module +Description = 'Build helper functions for Terminal.Gui.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.4.0' + +# Name of the PowerShell "host" subsystem (not system host name). Helps ensure that we know what to expect from the environment. +PowerShellHostName = 'ConsoleHost' + +# Minimum version of the PowerShell host required by this module +PowerShellHostVersion = '7.4.0' + +# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only. +# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else. +# Has nothing to do with runtime use of Terminal.Gui. +ProcessorArchitecture = 'Amd64' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @( + @{ + ModuleName='Microsoft.PowerShell.Utility' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='Microsoft.PowerShell.Management' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='PSReadLine' + ModuleVersion='2.3.4' + }, + "./Terminal.Gui.PowerShell.Core.psd1" +) + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules. +NestedModules = @('./Terminal.Gui.PowerShell.Build.psm1') + +# Functions to export from this module. +FunctionsToExport = @('Build-TerminalGui') + +# Cmdlets to export from this module. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module. +AliasesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/tree/v2_develop/Scripts/COPYRIGHT' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/gui-cs/Terminal.Gui' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'See change history and releases for Terminal.Gui on GitHub' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Scripts/Terminal.Gui.PowerShell.Build.psm1 b/Scripts/Terminal.Gui.PowerShell.Build.psm1 new file mode 100644 index 000000000..538b89548 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Build.psm1 @@ -0,0 +1,32 @@ +<# + .SYNOPSIS + Builds the Terminal.Gui library. + .DESCRIPTION + Builds the Terminal.Gui library. + Optional parameter sets are available to customize the build. + .PARAMETER versionBase + The base version for the Terminal.Gui library. +#> +Function Build-TerminalGui { + [CmdletBinding(SupportsShouldProcess, PositionalBinding=$false, DefaultParameterSetName="Basic", ConfirmImpact="Medium")] + [OutputType([bool],[PSObject])] + param( + [Parameter(Mandatory=$true)] + [Version]$versionBase, + [Parameter(Mandatory=$true, ParameterSetName="Custom")] + [switch]$Custom, + [Parameter(Mandatory=$false, ParameterSetName="Custom")] + [ValidateSet("Debug", "Release")] + [string]$slnBuildConfiguration = "Release", + [Parameter(Mandatory=$false, ParameterSetName="Custom")] + [ValidateSet("Any CPU", "x86"<#, "x64" #>)] + [string]$slnBuildPlatform = "Any CPU" + ) + + if(!$PSCmdlet.ShouldProcess("Building in $slnBuildConfiguration configuration for $slnBuildPlatform", "Terminal.Gui", "BUILDING")) { + return $null + } + + Write-Host NOT IMPLEMENTED. No Action has been taken. + return $false +} diff --git a/Scripts/Terminal.Gui.PowerShell.Core.psd1 b/Scripts/Terminal.Gui.PowerShell.Core.psd1 new file mode 100644 index 000000000..302005999 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Core.psd1 @@ -0,0 +1,138 @@ +# +# Module manifest for module 'Terminal.Gui.PowerShell' +# +# Generated by: Brandon Thetford (GitHub @dodexahedron) +# +# Generated on: 4/19/2024 +# + +@{ + +# No root module because this is a manifest module. +RootModule = '' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +CompatiblePSEditions = @('Core') + +# ID used to uniquely identify this module +GUID = 'c661fb12-70ae-4a9e-a95c-786a7980681d' + +# Author of this module +Author = 'Brandon Thetford (GitHub @dodexahedron)' + +# Company or vendor of this module +CompanyName = 'The Terminal.Gui Project' + +# Copyright statement for this module +Copyright = 'Brandon Thetford (GitHub @dodexahedron), provided to the Terminal.Gui project and you under the MIT license' + +# Description of the functionality provided by this module +Description = 'Utilities for development-time operations on and management of components of Terminal.Gui code and other assets.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.4.0' + +# Name of the PowerShell "host" subsystem (not system host name). Helps ensure that we know what to expect from the environment. +PowerShellHostName = 'ConsoleHost' + +# Minimum version of the PowerShell host required by this module +PowerShellHostVersion = '7.4.0' + +# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only. +# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else. +# Has nothing to do with runtime use of Terminal.Gui. +ProcessorArchitecture = 'Amd64' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @( + @{ + ModuleName='Microsoft.PowerShell.Utility' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='Microsoft.PowerShell.Management' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='PSReadLine' + ModuleVersion='2.3.4' + } +) + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @('./Terminal.Gui.PowerShell.Core.psm1') + +# Functions to export from this module. +FunctionsToExport = @('Open-Solution','Close-Solution') + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/gui-cs/Terminal.Gui/tree/v2_develop/Scripts/COPYRIGHT' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/gui-cs/Terminal.Gui' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'See change history and releases for Terminal.Gui on GitHub' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Scripts/Terminal.Gui.PowerShell.psm1 b/Scripts/Terminal.Gui.PowerShell.Core.psm1 similarity index 77% rename from Scripts/Terminal.Gui.PowerShell.psm1 rename to Scripts/Terminal.Gui.PowerShell.Core.psm1 index ec435a6f7..1c1574e74 100644 --- a/Scripts/Terminal.Gui.PowerShell.psm1 +++ b/Scripts/Terminal.Gui.PowerShell.Core.psm1 @@ -14,7 +14,7 @@ Function Open-Solution { [CmdletBinding()] param( [Parameter(Mandatory=$false, HelpMessage="The path to the solution file to open.")] - [Uri]$SolutionFilePath + [Uri]$SolutionFilePath = (Resolve-Path "../Terminal.sln") ) if(!$IsWindows) { @@ -44,6 +44,52 @@ Function Close-Solution { Remove-Variable vsProcesses } +<# + .SYNOPSIS + Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. + .DESCRIPTION + Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. + Also modifies the prompt to indicate the session has been altered. + Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. + .PARAMETER Debug + Minimally supported for Write-Debug calls in this function only. + .NOTES + Mostly does not respect common parameters like WhatIf, Confirm, etc. + This is just meant to be called by other scripts. + Calling this manually is not supported. +#> +Function Set-PowerShellEnvironment { + [CmdletBinding()] + param() + + # Set a custom prompt to indicate we're in our modified environment. + # Save the normal one first, though. + # And save it as ReadOnly and without the -Force parameter, so this will be skipped if run more than once in the same session without a reset. + New-Variable -Name NormalPrompt -Option ReadOnly -Scope Global -Value (Get-Item Function:prompt).ScriptBlock -ErrorAction SilentlyContinue + Set-Item Function:prompt { "TGPS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "; } + + # Save existing PSModulePath for optional reset later. + # If it is already saved, do not overwrite, but continue anyway. + New-Variable -Name OriginalPSModulePath -Visibility Public -Option ReadOnly -Scope Global -Value ($Env:PSModulePath) -ErrorAction SilentlyContinue + Write-Debug -Message "`$OriginalPSModulePath is $OriginalPSModulePath" -Debug:$DebugPreference + + # Get platform-specific path variable entry separator. Continue if it's already set. + New-Variable -Name PathVarSeparator -Visibility Public -Option ReadOnly -Scope Global -Value ";" -Description 'Separator character used in environment variables such as $Env:PSModulePath' -ErrorAction SilentlyContinue + + if(!$IsWindows) { + $PathVarSeparator = ':' + } + Write-Debug -Message "`$PathVarSeparator is $PathVarSeparator" -Debug:$DebugPreference + + # If Env:PSModulePath already has the current path, don't append it again. + if($Env:PSModulePath -notlike "*$((Resolve-Path .).Path)*") { + Write-Debug -Message "Appending $((Resolve-Path .).Path) to `$Env:PSModulePath" -Debug:$DebugPreference + $env:PSModulePath = Join-String -Separator $PathVarSeparator -InputObject @( $env:PSModulePath, (Resolve-Path .).Path ) + } + Write-Debug -Message "`$Env:PSModulePath is $Env:PSModulePath" -Debug:$DebugPreference +} + + <# .SYNOPSIS Resets changes made by ConfigureEnvironment.pst to the current PowerShell environment. @@ -65,8 +111,9 @@ Function Close-Solution { To exit the current session. Same as simply using the Exit command. #> Function Reset-PowerShellEnvironment { + [CmdletBinding(DefaultParameterSetName="Basic")] param( - [Parameter(Mandatory = $false)] + [Parameter(Mandatory=$false, ParameterSetName="Basic")] [switch]$Exit ) @@ -76,67 +123,21 @@ Function Reset-PowerShellEnvironment { if(Get-Variable -Name NormalPrompt -Scope Global -ErrorAction SilentlyContinue){ Set-Item Function:prompt $NormalPrompt - Remove-Variable -Name NormalPrompt -Scope Global -Force + Remove-Variable -Name NormalPrompt -Scope Global -Force -ErrorAction SilentlyContinue } if(Get-Variable -Name OriginalPSModulePath -Scope Global -ErrorAction SilentlyContinue){ $Env:PSModulePath = $OriginalPSModulePath - Remove-Variable -Name OriginalPSModulePath -Scope Global -Force + Remove-Variable -Name OriginalPSModulePath -Scope Global -Force -ErrorAction SilentlyContinue } Remove-Variable -Name PathVarSeparator -Scope Global -Force -ErrorAction SilentlyContinue } -<# - .SYNOPSIS - Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. - .DESCRIPTION - Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. - Also modifies the prompt to indicate the session has been altered. - Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. -#> - -<# - .SYNOPSIS - Sets up a standard environment for other Terminal.Gui.PowerShell scripts and modules. - .DESCRIPTION - Configures environment variables and global variables for other Terminal.Gui.PowerShell scripts to use. - Also modifies the prompt to indicate the session has been altered. - Reset changes by exiting the session or by calling Reset-PowerShellEnvironment or ./ResetEnvironment.ps1. -#> -Function Set-PowerShellEnvironment { - # Set a custom prompt to indicate we're in our modified environment. - # Save the normal one first, though. - New-Variable -Name NormalPrompt -Option ReadOnly -Scope Global -Value (Get-Item Function:prompt).ScriptBlock -ErrorAction SilentlyContinue - Set-Item Function:prompt { "TGPS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "; } - - # Save existing PSModulePath for optional reset later. - # If it is already saved, do not overwrite, but continue anyway. - New-Variable -Name OriginalPSModulePath -Visibility Public -Option ReadOnly -Scope Global -Value ($Env:PSModulePath) -ErrorAction SilentlyContinue - Write-Debug -Message "`$OriginalPSModulePath is $OriginalPSModulePath" - - # Get platform-specific path variable entry separator. Continue if it's already set. - New-Variable -Name PathVarSeparator -Visibility Public -Option ReadOnly,Constant -Scope Global -Value ";" -Description 'Separator character used in environment variables such as $Env:PSModulePath' -ErrorAction SilentlyContinue - - if(!$IsWindows) { - $PathVarSeparator = ':' - } - Write-Debug -Message "`$PathVarSeparator is $PathVarSeparator" - - # Now make it constant. - Set-Variable PathVarSeparator -Option Constant -ErrorAction SilentlyContinue - - # If Env:PSModulePath already has the current path, don't append it again. - if($Env:PSModulePath -notlike "*$((Resolve-Path .).Path)*") { - Write-Debug -Message "Appending $((Resolve-Path .).Path) to `$Env:PSModulePath" - $env:PSModulePath = Join-String -Separator $PathVarSeparator -InputObject @( $env:PSModulePath, (Resolve-Path .).Path ) - } - Write-Debug -Message "`$Env:PSModulePath is $Env:PSModulePath" -} - # This ensures the environment is reset when unloading the module. # Without this, function:prompt will be undefined. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Reset-PowerShellEnvironment - Pop-Location -} \ No newline at end of file +} + +Set-PowerShellEnvironment \ No newline at end of file diff --git a/Scripts/Terminal.Gui.PowerShell.Git.psd1 b/Scripts/Terminal.Gui.PowerShell.Git.psd1 new file mode 100644 index 000000000..1bfcd58a1 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Git.psd1 @@ -0,0 +1,135 @@ +# +# Module manifest for module 'Terminal.Gui.PowerShell.Git' +# +# Generated by: Brandon Thetford +# +# Generated on: 4/26/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = '' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +CompatiblePSEditions = 'Core' + +# ID used to uniquely identify this module +GUID = '33a6c4c9-c0a7-4c09-b171-1da0878f93ea' + +# Author of this module +Author = 'Brandon Thetford (GitHub @dodexahedron)' + +# Company or vendor of this module +CompanyName = 'The Terminal.Gui Project' + +# Copyright statement for this module +Copyright = 'Brandon Thetford (GitHub @dodexahedron), provided to the Terminal.Gui project and you under the MIT license' + +# Description of the functionality provided by this module +Description = 'Simple helper commands for common git operations.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.4' + +# Name of the PowerShell host required by this module +PowerShellHostName = 'ConsoleHost' + +# Minimum version of the PowerShell host required by this module +PowerShellHostVersion = '7.4.0' + +# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only. +# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else. +# Has nothing to do with runtime use of Terminal.Gui. +ProcessorArchitecture = 'AMD64' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @( + @{ + ModuleName='Microsoft.PowerShell.Utility' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='Microsoft.PowerShell.Management' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='PSReadLine' + ModuleVersion='2.3.4' + } +) + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +FormatsToProcess = @() + +# Modules to import as nested modules. +NestedModules = @("./Terminal.Gui.PowerShell.Git.psm1") + +# Functions to export from this module. +FunctionsToExport = @('New-GitBranch') + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +DscResourcesToExport = @() + +# List of all modules packaged with this module +ModuleList = @('./Terminal.Gui.PowerShell.Git.psm1') + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Scripts/Terminal.Gui.PowerShell.Git.psm1 b/Scripts/Terminal.Gui.PowerShell.Git.psm1 new file mode 100644 index 000000000..ef5f9c743 --- /dev/null +++ b/Scripts/Terminal.Gui.PowerShell.Git.psm1 @@ -0,0 +1,111 @@ +<# + .SYNOPSIS + Creates a new branch with the specified name. + .DESCRIPTION + Creates a new branch with the specified name. + .PARAMETER Name + The name of the new branch. + Always required. + Must match the .net regex pattern "v2_\d{4}_[a-zA-Z0-9()_-]+". + Must also otherwise be a valid identifier for a git branch and follow any other project guidelines. + .PARAMETER NoSwitch + If specified, does not automatically switch to your new branch after creating it. + Default is to switch to the new branch after creating it. + .PARAMETER Push + If specified, automatically pushes the new branch to your remote after creating it. + .PARAMETER Remote + The name of the git remote, as configured. + If you never explicitly set this yourself, it is typically "origin". + If you only have one remote defined or have not explicitly set a remote yourself, do not provide this parameter; It will be detected automatically. + .INPUTS + None + .OUTPUTS + The name of the current branch after the operation, as a String. + If NoSwitch was specified and the operation succeeded, this should be the source branch. + If NoSwith was not specified or was explicitly set to $false and the operation succeeded, this should be the new branch. + If an exception occurs, does not return. Exceptions are unhandled and are the responsibility of the caller. + .NOTES + Errors thrown by git commands are not explicitly handled. +#> +Function New-GitBranch { + [CmdletBinding(PositionalBinding=$false, SupportsShouldProcess=$true, ConfirmImpact="Low", DefaultParameterSetName="Basic")] + param( + [Parameter(Mandatory=$true, ParameterSetName="Basic")] + [Parameter(Mandatory=$true, ParameterSetName="NoSwitch")] + [Parameter(Mandatory=$true, ParameterSetName="Push")] + [ValidatePattern("v2_\d{4}_[a-zA-Z0-9()_-]+")] + [string]$Name, + [Parameter(Mandatory=$true,ParameterSetName="NoSwitch",DontShow)] + [switch]$NoSwitch, + [Parameter(Mandatory=$false, ParameterSetName="Basic")] + [Parameter(Mandatory=$true, ParameterSetName="Push")] + [switch]$Push, + [Parameter(Mandatory=$false, ParameterSetName="Push")] + [string]$Remote = $null + ) + $currentBranch = (& git branch --show-current) + + if(!$PSCmdlet.ShouldProcess("Creating new branch named $Name from $currentBranch", $Name, "Creating branch")) { + return $null + } + + git branch $Name + + if(!$NoSwitch) { + git switch $Name + + if($Push) { + if([String]::IsNullOrWhiteSpace($Remote)) { + $tempRemotes = (git remote show) + if($tempRemotes -is [array]){ + # If we've gotten here, Push was specified, a remote was not specified or was blank, and there are multiple remotes defined locally. + # Not going to support that. Just error out. + Remove-Variable tempRemotes + throw "No Remote specified and multiple remotes are defined. Cannot continue." + } else { + # Push is set, Remote wasn't, but there's only one defined. Safe to continue. Use the only remote. + $Remote = $tempRemotes + Remove-Variable tempRemotes + } + } + + # Push is set, and either Remote was specified or there's only one remote defined and we will use that. + # Perform the push. + git push --set-upstream $Remote $Name + } + } else{ + # NoSwitch was specified. + # Return the current branch name. + return $currentBranch + } + + # If we made it to this point, return the Name that was specified. + return $Name +} + +<# + .SYNOPSIS + Checks if the command 'git' is available in the current session. + .DESCRIPTION + Checks if the command 'git' is available in the current session. + Throws an error if not. + Returns $true if git is available. + Only intended for use in scripts and module manifests. + .INPUTS + None + .OUTPUTS + If git exists, $true. + Otherwise, $false. +#> +Function Test-GitAvailable { + [OutputType([Boolean])] + [CmdletBinding()] + param() + if($null -eq (Get-Command git -ErrorAction Ignore)) { + Write-Error -Message "git was not found. Git functionality will not work." -Category ObjectNotFound -TargetObject "git" + return $false + } + return $true +} + +Test-GitAvailable -ErrorAction Continue \ No newline at end of file diff --git a/Scripts/Terminal.Gui.PowerShell.psd1 b/Scripts/Terminal.Gui.PowerShell.psd1 index c496477dd..d90db54d9 100644 --- a/Scripts/Terminal.Gui.PowerShell.psd1 +++ b/Scripts/Terminal.Gui.PowerShell.psd1 @@ -1,15 +1,20 @@ -# -# Module manifest for module 'Terminal.Gui.PowerShell' -# -# Generated by: Brandon Thetford (GitHub @dodexahedron) -# -# Generated on: 4/19/2024 -# +<# + .SYNOPSIS + All-inclusive module that includes all other Terminal.Gui.PowerShell.* modules. + .DESCRIPTION + All-inclusive module that includes all other Terminal.Gui.PowerShell.* modules. + .EXAMPLE + Import-Module ./Terminal.Gui.PowerShell.psd1 + .NOTES + Doc comments on manifest files are not supported by Get-Help as of PowerShell 7.4.2. + This comment block is purely informational and will not interfere with module loading. +#> + @{ -# Script module or binary module file associated with this manifest. -RootModule = 'Terminal.Gui.PowerShell.psm1' +# No root module because this is a manifest module. +RootModule = '' # Version number of this module. ModuleVersion = '1.0.0' @@ -44,10 +49,23 @@ PowerShellHostVersion = '7.4.0' # Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only. # Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else. # Has nothing to do with runtime use of Terminal.Gui. -ProcessorArchitecture = 'AMD64' +ProcessorArchitecture = 'Amd64' # Modules that must be imported into the global environment prior to importing this module -RequiredModules = @('Microsoft.PowerShell.Utility','Microsoft.PowerShell.Management','PSReadLine') +RequiredModules = @( + @{ + ModuleName='Microsoft.PowerShell.Utility' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='Microsoft.PowerShell.Management' + ModuleVersion='7.0.0' + }, + @{ + ModuleName='PSReadLine' + ModuleVersion='2.3.4' + } +) # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() @@ -61,26 +79,32 @@ RequiredModules = @('Microsoft.PowerShell.Utility','Microsoft.PowerShell.Managem # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -NestedModules = @('./Terminal.Gui.PowerShell.Analyzers.psd1') +# Modules to import as nested modules of this module. +# This module is just a shortcut that loads all of our modules. +NestedModules = @('./Terminal.Gui.PowerShell.Core.psd1', './Terminal.Gui.PowerShell.Analyzers.psd1', './Terminal.Gui.PowerShell.Git.psd1', './Terminal.Gui.PowerShell.Build.psd1') -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @('Build-Analyzers','Close-Solution','Open-Solution','Reset-PowerShellEnvironment','Set-PowerShellEnvironment') -#FunctionsToExport = @('*') +# Functions to export from this module. +# Not filtered, so exports all functions exported by all nested modules. +FunctionsToExport = '*' -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +# Cmdlets to export from this module. +# We don't have any, so empty array. CmdletsToExport = @() -# Variables to export from this module +# Variables to export from this module. +# We explicitly control scope of variables, so empty array. VariablesToExport = @() -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +# Aliases to export from this module. +# None defined at this time. AliasesToExport = @() # List of all modules packaged with this module +# This is informational ONLY, so it's just blank right now. # ModuleList = @() # List of all files packaged with this module +# This is informational ONLY, so it's just blank right now. # FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 b/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 index c8477105d..b5eced04c 100644 --- a/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 +++ b/Scripts/Terminal.Gui.Powershell.Analyzers.psd1 @@ -9,7 +9,7 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'Terminal.Gui.PowerShell.Analyzers.psm1' +RootModule = '' # Version number of this module. ModuleVersion = '1.0.0' @@ -45,7 +45,7 @@ PowerShellHostName = 'ConsoleHost' ProcessorArchitecture = 'Amd64' # Modules that must be imported into the global environment prior to importing this module -RequiredModules = @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Utility') +RequiredModules = @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Utility','./Terminal.Gui.PowerShell.Core.psd1') # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() @@ -59,13 +59,13 @@ RequiredModules = @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Util # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() +# Modules to import as nested modules. +NestedModules = @('./Terminal.Gui.PowerShell.Analyzers.psm1') -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +# Functions to export from this module. FunctionsToExport = @('Build-Analyzers') -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +# Cmdlets to export from this module. CmdletsToExport = @() # Variables to export from this module @@ -74,15 +74,6 @@ VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @() -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -FileList = './Terminal.Gui.Powershell.Analyzers.psm1' - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ From 9a6c12d2aed59f449243683b8d2d1ea81d04550c Mon Sep 17 00:00:00 2001 From: Brandon Thetford Date: Fri, 26 Apr 2024 21:57:53 -0700 Subject: [PATCH 19/19] Add README.md for Scripts directory --- Scripts/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Scripts/README.md diff --git a/Scripts/README.md b/Scripts/README.md new file mode 100644 index 000000000..288035465 --- /dev/null +++ b/Scripts/README.md @@ -0,0 +1,46 @@ +## Development and Design-Time PowerShell Modules +This directory contains PowerShell modules for use when working with Terminal.sln + +### Purpose +These modules will be modifed and extended as time goes on, whenever someone decides to add something to make life easier. + +### Requirements +These modules are designed for **PowerShell Core, version 7.4 or higher**, on any platform, and must be run directly within a pwsh process.\ +If you want to use them from within another application, such as PowerShell hosted inside VSCode, you must first run `pwsh` in that terminal. + +As the primary development environment for Terminal.Gui is Visual Studio 2022+, some functionality may be limited, unavailable, or not work on platforms other than Windows.\ +Most should still work on Linux, however.\ +Functions which are platform-specific will be documented as such in their Get-Help documentation. + +Specific requirements for each module can be found in the module manifests and will be automatically imported or, if unavailable, PowerShell will tell you what's missing. + +### Usage +From a PowerShell 7.4 or higher prompt, navigate to your Terminal.Gui repository directory, and then into the Scripts directory (the same directory as this document). + +#### Import Module and Configure Environment +Run the following command to import all Terminal.Gui.PowerShell.* modules: +```powershell +Import-Module ./Terminal.Gui.PowerShell.psd1 +``` +If the environment meets the requirements, the modules will now be loaded into the current powershell session and exported commands will be immediately available for use. + +#### Getting Help +All exported functions and commandlets are provided with full PowerShell help annotations compatible with `Get-Help`. + +See [The Get-Help documentation at Microsoft Learn]([https://](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-help?view=powershell-7.4)) for Get-Help information. + +#### Cleaning Up/Resetting Environment +No environment changes made by the modules on import are persistent. + +When you are finished using the modules, you can optionally unload the modules, which will also reset the configuration changes made on import, by simply exiting the PowerShell session (`exit`) or by running the following command:\ +**NOTE DIFFERENT TEXT FROM IMPORT COMMAND!** +```powershell +Remove-Module Terminal.Gui.PowerShell +``` + +### LICENSE +MIT License + +Original Author: Brandon Thetford (@dodexahedron) + +See COPYRIGHT in this directory for license text.