diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..487ec02 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*] +# This only shows when calling AnalyzerConfigOptionsProvider.GetOptions(additionalText) +example_editorconfig_value = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d016234..ca9f810 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: env: BUILD_CONFIG: 'Release' - SOLUTION: 'SourceGeneratorContext.sln' + SOLUTION: '*.sln' runs-on: ubuntu-latest @@ -33,7 +33,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Restore dependencies run: dotnet restore $SOLUTION @@ -44,27 +44,32 @@ jobs: - name: Run tests run: dotnet test --configuration $BUILD_CONFIG --no-restore --no-build --verbosity normal + - name: Pack NuGet packages + run: | + dotnet pack $SOLUTION --configuration $BUILD_CONFIG --no-build --output ./artifacts + echo "=== Packages created ===" + ls -la ./artifacts/ + - name: Get tag for current commit id: get_tag + # Check for tags when triggered by main branch push (with tag) or direct tag push + # Can't use github.ref_name because it's "main" when pushing branch+tag together if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') - run: | - TAG=$(git tag --points-at HEAD | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "Found tag: $TAG" - - - name: Pack NuGet package - if: steps.get_tag.outputs.tag != '' - run: dotnet pack $SOLUTION --configuration $BUILD_CONFIG --no-build --output ./artifacts + uses: olegtarasov/get-tag@v2.1.4 - - name: Publish to NuGet + - name: Extract release notes from CHANGELOG.md if: steps.get_tag.outputs.tag != '' - run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + id: extract_notes + uses: mindsers/changelog-reader-action@v2 + with: + version: ${{ steps.get_tag.outputs.tag }} + path: ./CHANGELOG.md - name: Create GitHub Release if: steps.get_tag.outputs.tag != '' uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.get_tag.outputs.tag }} - generate_release_notes: true + body: ${{ steps.extract_notes.outputs.changes }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..76f135a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +name: Publish to NuGet + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore, Build, and Pack + run: | + dotnet restore *.sln + dotnet build *.sln --configuration Release --no-restore + dotnet pack *.sln --configuration Release --no-build --output ./artifacts + + - name: Publish to NuGet + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore index 35063fc..66a5b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,13 @@ CodeCoverage/ # NUnit *.VisualState.xml TestResult.xml -nunit-*.xml \ No newline at end of file +nunit-*.xml + +# Visual Studio Code +.vscode/ + +# Rider +.idea/ + +# Visual Studio +.vs/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fae3586 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- `SourceGeneratorContext` library to help creators of source generators. +- `[SourceGeneratorContext]` attribute to mark partial classes for generation. +- Generate doc-comments to partial classes showing different portions of the context available to source generators. + - IncludeAll + - include all available details. + - IncludeAttributeContextTargetSymbol + - GeneratorAttributeSyntaxContext.TargetSymbol details + - IncludeAttributeContextTypeSymbol + - GeneratorAttributeSyntaxContext.TargetSymbol as ITypeSymbol details + - IncludeAttributeContextNamedTypeSymbol + - GeneratorAttributeSyntaxContext.TargetSymbol as INamedTypeSymbol details + - IncludeAttributeContextTargetNode + - GeneratorAttributeSyntaxContext.TargetNode details + - IncludeAttributeContextAttributes + - GeneratorAttributeSyntaxContext.Attributes details + - IncludeAttributeContextAllAttributes + - GeneratorAttributeSyntaxContext.GetAttributes() details + - IncludeGlobalOptions + - AnalyzerConfigOptionsProvider's GlobalOptions details + - IncludeCompilation + - CompilationProvider's Compilation details + - IncludeCompilationOptions + - CompilationProvider's Compilation.Options details + - IncludeCompilationAssembly + - CompilationProvider's Compilation.Assembly details + - IncludeCompilationReferences + - Counts of CompilationProvider's: + - Compilation.References + - Compilation.DirectiveReferences + - Compilation.ExternalReferences + - Compilation.ReferencedAssemblyNames + - IncludeParseOptions + - ParseOptionsProvider's ParseOptions details + - IncludeAdditionalTexts + - AdditionalTextsProvider's AdditionalText details + - IncludeAdditionalTextsOptions + - AdditionalTextsProvider's AdditionalText details combined with AnalyzerConfigOptionsProvider's AnalyzerConfigOptions for the AdditionalText + - IncludeMetadataReferences + - MetadataReferencesProvider's MetadataReference details +- Diagnostic log of the source generation process and timing included. + +[Unreleased]: https://github.com/datacute/SourceGeneratorContext/compare/main...develop diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..9954b4c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + + + + Stephen Denne + Copyright © Stephen Denne 2024, 2025 + Datacute + https://github.com/datacute/SourceGeneratorContext + See full release notes and changelog: $(PackageProjectUrl)/blob/main/CHANGELOG.md + false + + + + true + $(MSBuildThisFileDirectory)\artifacts + + + \ No newline at end of file diff --git a/SourceGeneratorContext.Attribute/SourceGeneratorContext.Attribute.csproj b/SourceGeneratorContext.Attribute/SourceGeneratorContext.Attribute.csproj new file mode 100644 index 0000000..7465efd --- /dev/null +++ b/SourceGeneratorContext.Attribute/SourceGeneratorContext.Attribute.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + 12 + Datacute.SourceGeneratorContext.Attribute + Datacute.SourceGeneratorContext + enable + 1.0.0 + + + + true + + + diff --git a/SourceGeneratorContext.Attribute/SourceGeneratorContextAttribute.cs b/SourceGeneratorContext.Attribute/SourceGeneratorContextAttribute.cs new file mode 100644 index 0000000..a207bc5 --- /dev/null +++ b/SourceGeneratorContext.Attribute/SourceGeneratorContextAttribute.cs @@ -0,0 +1,134 @@ +using System; +// ReSharper disable UnusedAutoPropertyAccessor.Global Properties getters are not used as the source generator reads the source code. +// ReSharper disable UnusedParameter.Local Unused parameters are used to demonstrate behaviour. + +namespace Datacute.SourceGeneratorContext; + +/// +/// Add this attribute to a partial class to generate doc-comments detailing the source generation context. +/// +[System.Diagnostics.Conditional("DATACUTE_SOURCEGENERATORCONTEXTATTRIBUTE_USAGES")] +[AttributeUsage( + validOn: AttributeTargets.Class | + AttributeTargets.Interface | + AttributeTargets.Struct, // Method and Property should be allowed too + Inherited = true, // Inherited to show how SyntaxProvider.ForAttributeWithMetadataName doesn't support inheritance + AllowMultiple = true)] // AllowMultiple to show the differences when multiple attributes are applied +public class SourceGeneratorContextAttribute : Attribute +{ + /// + /// There is a huge amount of information available, but Visual Studio does not scroll doc-comments. + /// So either IncludeAll and view the generated source, or set one of the named parameters to control what gets output: + /// + /// [SourceGeneratorContext(IncludeAll = true)] + /// internal partial class Example; + /// + /// + public SourceGeneratorContextAttribute() + { + } + + /// + /// Set to true to include all available details. + /// + public bool IncludeAll { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.TargetSymbol details. + /// + public bool IncludeAttributeContextTargetSymbol { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.TargetSymbol as ITypeSymbol details. + /// + public bool IncludeAttributeContextTypeSymbol { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.TargetSymbol as INamedTypeSymbol details. + /// + public bool IncludeAttributeContextNamedTypeSymbol { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.TargetNode details. + /// + public bool IncludeAttributeContextTargetNode { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.Attributes details. + /// + public bool IncludeAttributeContextAttributes { get; set; } + + /// + /// Set to true to include the GeneratorAttributeSyntaxContext.GetAttributes() details. + /// + public bool IncludeAttributeContextAllAttributes { get; set; } + + /// + /// Set to true to include the AnalyzerConfigOptionsProvider's GlobalOptions details. + /// + public bool IncludeGlobalOptions { get; set; } + + /// + /// Set to true to include the CompilationProvider's Compilation details. + /// + public bool IncludeCompilation { get; set; } + + /// + /// Set to true to include the CompilationProvider's Compilation.Options details. + /// + public bool IncludeCompilationOptions { get; set; } + + /// + /// Set to true to include the CompilationProvider's Compilation.Assembly details. + /// + public bool IncludeCompilationAssembly { get; set; } + + /// + /// Set to true to include the Counts of CompilationProvider's Compilation.References, Compilation.DirectiveReferences, Compilation.ExternalReferences, and Compilation.ReferencedAssemblyNames. + /// + public bool IncludeCompilationReferences { get; set; } + + /// + /// Set to true to include the ParseOptionsProvider's ParseOptions details. + /// + public bool IncludeParseOptions { get; set; } + + /// + /// Set to true to include the AdditionalTextsProvider's AdditionalText details. + /// + public bool IncludeAdditionalTexts { get; set; } + + /// + /// Set to true to include the AdditionalTextsProvider's AdditionalText details combined with AnalyzerConfigOptionsProvider's AnalyzerConfigOptions for the AdditionalText. + /// + public bool IncludeAdditionalTextsOptions { get; set; } + + /// + /// Set to true to include the MetadataReferencesProvider's MetadataReference details. + /// + public bool IncludeMetadataReferences { get; set; } + + + #region Demonstration purposes only + + /// + /// Example of a named parameter. + /// + public string ExampleNamedParameter { get; set; } = + string.Empty; // only used for demonstrating working with Named Parameters + + /// + /// Example of an optional parameter. + /// + /// + public + SourceGeneratorContextAttribute( + string? exampleOptionalParameter = + null) // only used for demonstrating working with Constructor Arguments + { + // The constructor arguments do not need to be assigned to fields or properties + // as the source of the supplied values is what is available to the source generator + } + + #endregion +} \ No newline at end of file diff --git a/SourceGeneratorContext.sln b/SourceGeneratorContext.sln new file mode 100644 index 0000000..2f93387 --- /dev/null +++ b/SourceGeneratorContext.sln @@ -0,0 +1,42 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35825.156 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratorContext", "SourceGeneratorContext\SourceGeneratorContext.csproj", "{DB6CCC3F-D197-48F7-B166-E9194233939D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratorContext.Attribute", "SourceGeneratorContext.Attribute\SourceGeneratorContext.Attribute.csproj", "{394AEA19-BF98-4427-BE95-BBD90A7D65A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratorContextExample", "SourceGeneratorContextExample\SourceGeneratorContextExample.csproj", "{56E79762-6BB4-4042-9EAB-2D819C2FDAB0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props + LICENSE = LICENSE + version.props = version.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB6CCC3F-D197-48F7-B166-E9194233939D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB6CCC3F-D197-48F7-B166-E9194233939D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB6CCC3F-D197-48F7-B166-E9194233939D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB6CCC3F-D197-48F7-B166-E9194233939D}.Release|Any CPU.Build.0 = Release|Any CPU + {394AEA19-BF98-4427-BE95-BBD90A7D65A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {394AEA19-BF98-4427-BE95-BBD90A7D65A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {394AEA19-BF98-4427-BE95-BBD90A7D65A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {394AEA19-BF98-4427-BE95-BBD90A7D65A5}.Release|Any CPU.Build.0 = Release|Any CPU + {56E79762-6BB4-4042-9EAB-2D819C2FDAB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56E79762-6BB4-4042-9EAB-2D819C2FDAB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56E79762-6BB4-4042-9EAB-2D819C2FDAB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56E79762-6BB4-4042-9EAB-2D819C2FDAB0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SourceGeneratorContext.sln.DotSettings.user b/SourceGeneratorContext.sln.DotSettings.user new file mode 100644 index 0000000..ad15185 --- /dev/null +++ b/SourceGeneratorContext.sln.DotSettings.user @@ -0,0 +1,47 @@ + + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + \ No newline at end of file diff --git a/SourceGeneratorContext/AdditionalTextDescription.cs b/SourceGeneratorContext/AdditionalTextDescription.cs new file mode 100644 index 0000000..974fa5d --- /dev/null +++ b/SourceGeneratorContext/AdditionalTextDescription.cs @@ -0,0 +1,53 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct AdditionalTextDescription +{ + public readonly string DocComments; + public readonly string OptionsComments; + + public AdditionalTextDescription(AdditionalText additionalText, AnalyzerConfigOptions? options = null) + { + var sb = new StringBuilder(); + sb.AddComment("Path", additionalText.Path); + var sourceText = additionalText.GetText(); + sb.AddComment("Length", sourceText?.Length); + sb.AddComment("Encoding", sourceText?.Encoding?.EncodingName); + sb.AddComment("Lines", sourceText?.Lines.Count); + sb.AddComment("ChecksumAlgorithm", sourceText?.ChecksumAlgorithm); + sb.AddComment("CanBeEmbedded", sourceText?.CanBeEmbedded); + + DocComments = sb.ToString(); + + sb.Clear(); + + if (options != null) + { + foreach (var key in options.Keys) + { + var v = options.TryGetValue(key, out var value) ? value : string.Empty; + sb.AddComment(key, v); + } + } + + OptionsComments = sb.ToString(); + } + + public static AdditionalTextDescription Select(AdditionalText additionalText, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return new AdditionalTextDescription(additionalText); + } + + public static AdditionalTextDescription Select((AdditionalText additionalText, AnalyzerConfigOptionsProvider optionsProvider) args, CancellationToken token) + { + LightweightTrace.Add(TrackingNames.AdditionalTextDescription_Select); + + token.ThrowIfCancellationRequested(); + var options = args.optionsProvider.GetOptions(args.additionalText); + return new AdditionalTextDescription(args.additionalText, options); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/AnalyzerConfigOptionsDescription.cs b/SourceGeneratorContext/AnalyzerConfigOptionsDescription.cs new file mode 100644 index 0000000..f6087df --- /dev/null +++ b/SourceGeneratorContext/AnalyzerConfigOptionsDescription.cs @@ -0,0 +1,28 @@ +using System.Text; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct AnalyzerConfigOptionsDescription +{ + public readonly string DocComments; + + public AnalyzerConfigOptionsDescription(AnalyzerConfigOptions options) + { + var sb = new StringBuilder(); + foreach (var key in options.Keys) + { + var v = options.TryGetValue(key, out var value) ? value : string.Empty; + sb.AddComment(key, v); + } + DocComments = sb.ToString(); + } + + public static AnalyzerConfigOptionsDescription Select(AnalyzerConfigOptionsProvider provider, CancellationToken token) + { + LightweightTrace.Add(TrackingNames.AnalyzerConfigOptionsDescription_Select); + + token.ThrowIfCancellationRequested(); + return new AnalyzerConfigOptionsDescription(provider.GlobalOptions); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/AttributeContext.cs b/SourceGeneratorContext/AttributeContext.cs new file mode 100644 index 0000000..0a354eb --- /dev/null +++ b/SourceGeneratorContext/AttributeContext.cs @@ -0,0 +1,427 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct AttributeContext +{ + // Store parent classes with their modifiers + public record struct ParentClassInfo( + string Name, + bool IsStatic, + Accessibility Accessibility, + string RecordStructOrClass, + string[] TypeParameters); + public readonly ParentClassInfo[] ParentClasses { get; } + public bool HasParentClasses => ParentClasses.Length > 0; + + public readonly bool IncludeSummary; + public readonly bool IncludeAll; + + public readonly bool IncludeAttributeContextTargetSymbol; + public readonly bool IncludeAttributeContextTypeSymbol; + public readonly bool IncludeAttributeContextNamedTypeSymbol; + public readonly bool IncludeAttributeContextTargetNode; + public readonly bool IncludeAttributeContextAttributes; + public readonly bool IncludeAttributeContextAllAttributes; + + public readonly bool IncludeGlobalOptions; + public readonly bool IncludeCompilation; + public readonly bool IncludeCompilationOptions; + public readonly bool IncludeCompilationAssembly; + public readonly bool IncludeCompilationReferences; + public readonly bool IncludeParseOptions; + public readonly bool IncludeAdditionalTexts; + public readonly bool IncludeAdditionalTextsOptions; + public readonly bool IncludeMetadataReferences; + + public readonly string TargetSymbolDocComments; + public readonly string TypeSymbolDocComments; + public readonly string NamedTypeSymbolDocComments; + public readonly string TargetNodeDocComments; + public readonly string AttributesDocComments; + public readonly string AllAttributesDocComments; + + public readonly bool ContainingNamespaceIsGlobalNamespace; + public readonly string ContainingNamespaceDisplayString; + + public readonly Accessibility DeclaredAccessibility; // public + public readonly bool IsStatic; // static + public readonly string RecordStructOrClass; // (partial) class + public readonly string Name; // ClassName + public readonly string[] TypeParameters; // + public readonly string DisplayString; // Namespace.ClassName + + public AttributeContext(in GeneratorAttributeSyntaxContext generatorAttributeSyntaxContext) + { + var sb = new StringBuilder(); + var targetSymbol = generatorAttributeSyntaxContext.TargetSymbol; + sb.AddComment("Type", targetSymbol.GetType().Name); + sb.AddComment("Kind", targetSymbol.Kind); + sb.AddComment("Language", targetSymbol.Language); + sb.AddComment("DeclaredAccessibility", targetSymbol.DeclaredAccessibility); + sb.AddComment("ContainingSymbol Kind", targetSymbol.ContainingSymbol?.Kind); + sb.AddComment("ContainingSymbol Name", targetSymbol.ContainingSymbol?.Name); + sb.AddComment("ContainingAssembly Name", targetSymbol.ContainingAssembly?.Name); + sb.AddComment("ContainingModule Name", targetSymbol.ContainingModule?.Name); + sb.AddComment("ContainingType Name", targetSymbol.ContainingType?.Name); + var containingTypeTypeParameters = targetSymbol.ContainingType?.TypeParameters; + sb.AddComment("ContainingType Generic Types", containingTypeTypeParameters?.Length); + if (containingTypeTypeParameters != null) + { + for (var n = 0; n < containingTypeTypeParameters.Value.Length; n++) + { + var typeParameter = containingTypeTypeParameters.Value[n]; + sb.AddComment($"ContainingType Generic Type {n+1}", typeParameter.ToDisplayString()); + } + } + sb.AddComment("ContainingNamespace Name", targetSymbol.ContainingNamespace.Name); + sb.AddComment("ContainingNamespace IsGlobalNamespace", targetSymbol.ContainingNamespace.IsGlobalNamespace); + sb.AddComment("Name", targetSymbol.Name); + sb.AddComment("MetadataName", targetSymbol.MetadataName); + sb.AddComment("MetadataToken", targetSymbol.MetadataToken); + sb.AddComment("IsDefinition", targetSymbol.IsDefinition); + sb.AddComment("IsStatic", targetSymbol.IsStatic); + sb.AddComment("IsVirtual", targetSymbol.IsVirtual); + sb.AddComment("IsOverride", targetSymbol.IsOverride); + sb.AddComment("IsAbstract", targetSymbol.IsAbstract); + sb.AddComment("IsSealed", targetSymbol.IsSealed); + sb.AddComment("IsExtern", targetSymbol.IsExtern); + sb.AddComment("IsImplicitlyDeclared", targetSymbol.IsImplicitlyDeclared); + sb.AddComment("CanBeReferencedByName", targetSymbol.CanBeReferencedByName); + + TargetSymbolDocComments = sb.ToString(); + + sb.Clear(); + + if (generatorAttributeSyntaxContext.TargetSymbol is ITypeSymbol typeTargetSymbol) + { + sb.AddComment("TypeKind", typeTargetSymbol.TypeKind); + sb.AddComment("BaseType Name", typeTargetSymbol.BaseType?.Name ?? string.Empty); + var interfaces = typeTargetSymbol.Interfaces; + sb.AddComment("Interfaces Length", interfaces.Length); + for (var n = 0; n < interfaces.Length; n++) + { + var directInterface = interfaces[n]; + sb.AddComment($"Interface {n+1}", directInterface.ToDisplayString()); + } + var allInterfaces = typeTargetSymbol.AllInterfaces; + sb.AddComment("AllInterfaces Length", allInterfaces.Length); + for (var n = 0; n < allInterfaces.Length; n++) + { + var implementedInterface = allInterfaces[n]; + sb.AddComment($"Interface {n+1}", implementedInterface.ToDisplayString()); + } + sb.AddComment("IsReferenceType", typeTargetSymbol.IsReferenceType); + sb.AddComment("IsValueType", typeTargetSymbol.IsValueType); + sb.AddComment("IsAnonymousType", typeTargetSymbol.IsAnonymousType); + sb.AddComment("IsTupleType", typeTargetSymbol.IsTupleType); + sb.AddComment("IsNativeIntegerType", typeTargetSymbol.IsNativeIntegerType); + sb.AddComment("SpecialType", typeTargetSymbol.SpecialType); + sb.AddComment("IsRefLikeType", typeTargetSymbol.IsRefLikeType); + sb.AddComment("IsUnmanagedType", typeTargetSymbol.IsUnmanagedType); + sb.AddComment("IsReadOnly", typeTargetSymbol.IsReadOnly); + sb.AddComment("IsRecord", typeTargetSymbol.IsRecord); + sb.AddComment("NullableAnnotation", typeTargetSymbol.NullableAnnotation); + + TypeSymbolDocComments = sb.ToString(); + + sb.Clear(); + } + else + { + TypeSymbolDocComments = string.Empty; + } + + if (generatorAttributeSyntaxContext.TargetSymbol is INamedTypeSymbol namedTypeTargetSymbol) + { + TypeParameters = namedTypeTargetSymbol.TypeParameters.Select(tp => tp.Name).ToArray(); + sb.AddComment("Arity", namedTypeTargetSymbol.Arity); + sb.AddComment("IsGenericType", namedTypeTargetSymbol.IsGenericType); + sb.AddComment("IsUnboundGenericType", namedTypeTargetSymbol.IsUnboundGenericType); + sb.AddComment("IsScriptClass", namedTypeTargetSymbol.IsScriptClass); + sb.AddComment("IsImplicitClass", namedTypeTargetSymbol.IsImplicitClass); + sb.AddComment("IsComImport", namedTypeTargetSymbol.IsComImport); + sb.AddComment("IsFileLocal", namedTypeTargetSymbol.IsFileLocal); + sb.AddComment("MemberNames", namedTypeTargetSymbol.MemberNames.Count()); + sb.AddComment("TypeParameters", TypeParameters.Length); + for (var n = 0; n < TypeParameters.Length; n++) + { + var typeParameter = TypeParameters[n]; + sb.AddComment($"TypeParameter {n+1}", typeParameter); + } + sb.AddComment("InstanceConstructors", namedTypeTargetSymbol.InstanceConstructors.Length); + sb.AddComment("StaticConstructors", namedTypeTargetSymbol.StaticConstructors.Length); + sb.AddComment("MightContainExtensionMethods", namedTypeTargetSymbol.MightContainExtensionMethods); + sb.AddComment("IsSerializable", namedTypeTargetSymbol.IsSerializable); + + NamedTypeSymbolDocComments = sb.ToString(); + + sb.Clear(); + } + else + { + TypeParameters = Array.Empty(); + NamedTypeSymbolDocComments = string.Empty; + } + + var targetNode = generatorAttributeSyntaxContext.TargetNode; + sb.AddComment("Type", targetNode.GetType().Name); + sb.AddComment("RawKind", targetNode.RawKind); + sb.AddComment("Kind", targetNode.Kind()); + sb.AddComment("Language", targetNode.Language); + sb.AddComment("Span.Start", targetNode.Span.Start); + sb.AddComment("Span.Length", targetNode.Span.Length); + sb.AddComment("ContainsAnnotations", targetNode.ContainsAnnotations); + sb.AddComment("ContainsDiagnostics", targetNode.ContainsDiagnostics); + sb.AddComment("ContainsDirectives", targetNode.ContainsDirectives); + sb.AddComment("ContainsSkippedText", targetNode.ContainsSkippedText); + sb.AddComment("IsMissing", targetNode.IsMissing); + sb.AddComment("HasLeadingTrivia", targetNode.HasLeadingTrivia); + sb.AddComment("HasStructuredTrivia", targetNode.HasStructuredTrivia); + sb.AddComment("HasTrailingTrivia", targetNode.HasTrailingTrivia); + sb.AddComment("IsStructuredTrivia", targetNode.IsStructuredTrivia); + + + TargetNodeDocComments = sb.ToString(); + + sb.Clear(); + + ( + IncludeSummary, + IncludeAll, + IncludeAttributeContextTargetSymbol, + IncludeAttributeContextTypeSymbol, + IncludeAttributeContextNamedTypeSymbol, + IncludeAttributeContextTargetNode, + IncludeAttributeContextAttributes, + IncludeAttributeContextAllAttributes, + IncludeGlobalOptions, + IncludeCompilation, + IncludeCompilationOptions, + IncludeCompilationAssembly, + IncludeCompilationReferences, + IncludeParseOptions, + IncludeAdditionalTexts, + IncludeAdditionalTextsOptions, + IncludeMetadataReferences) = AddAttributes(sb, generatorAttributeSyntaxContext.Attributes, true); + + AttributesDocComments = sb.ToString(); + + sb.Clear(); + + AddAttributes(sb, targetSymbol.GetAttributes(), false); + + AllAttributesDocComments = sb.ToString(); + + sb.Clear(); + + // Repeated above, but pulled out for ease of code generation + var attributeTargetSymbol = (ITypeSymbol)generatorAttributeSyntaxContext.TargetSymbol; + + ContainingNamespaceIsGlobalNamespace = attributeTargetSymbol.ContainingNamespace.IsGlobalNamespace; + ContainingNamespaceDisplayString = attributeTargetSymbol.ContainingNamespace.ToDisplayString(); + + DeclaredAccessibility = attributeTargetSymbol.DeclaredAccessibility; + IsStatic = attributeTargetSymbol.IsStatic; + RecordStructOrClass = GetRecordStructOrClass(attributeTargetSymbol); + Name = attributeTargetSymbol.Name; + DisplayString = attributeTargetSymbol.ToDisplayString(); + + // Parse parent classes from symbol's containing types + var parentClasses = new List(); + var containingType = attributeTargetSymbol.ContainingType; + while (containingType != null) + { + var typeParams = containingType.TypeParameters.Select(tp => tp.Name).ToArray(); + + parentClasses.Insert(0, new ParentClassInfo( + containingType.Name, + containingType.IsStatic, + containingType.DeclaredAccessibility, + GetRecordStructOrClass(containingType), + typeParams)); + containingType = containingType.ContainingType; + } + + ParentClasses = parentClasses.ToArray(); + } + + private static ( + bool includeSummary, + bool includeAll, + bool includeAttributeContextTargetSymbol, + bool includeAttributeContextTypeSymbol, + bool includeAttributeContextNamedTypeSymbol, + bool includeAttributeContextTargetNode, + bool includeAttributeContextAttributes, + bool includeAttributeContextAllAttributes, + bool includeGlobalOptions, + bool includeCompilation, + bool includeCompilationOptions, + bool includeCompilationAssembly, + bool includeCompilationReferences, + bool includeParseOptions, + bool includeAdditionalTexts, + bool includeAdditionalTextsOptions, + bool includeMetadataReferences) + AddAttributes(StringBuilder sb, ImmutableArray attributes, bool capture) + { + bool includeAll = false; + bool includeSummary = true; + bool includeAttributeContextTargetSymbol = false; + bool includeAttributeContextTypeSymbol = false; + bool includeAttributeContextNamedTypeSymbol = false; + bool includeAttributeContextTargetNode = false; + bool includeAttributeContextAttributes = false; + bool includeAttributeContextAllAttributes = false; + bool includeGlobalOptions = false; + bool includeCompilation = false; + bool includeCompilationOptions = false; + bool includeCompilationAssembly = false; + bool includeCompilationReferences = false; + bool includeParseOptions = false; + bool includeAdditionalTexts = false; + bool includeAdditionalTextsOptions = false; + bool includeMetadataReferences = false; + + sb.AddComment("Attribute Count", attributes.Length); + for (var i = 0; i < attributes.Length; i++) + { + var attribute = attributes[i]; + sb.AddComment($"[{i}] AttributeClass", attribute.AttributeClass?.ToDisplayString()); + + var constructorArguments = attribute.ConstructorArguments; + var constructorArgumentsLength = constructorArguments.Length; + sb.AddComment($"[{i}] ConstructorArguments Count", constructorArgumentsLength); + for (var c = 0; c < constructorArgumentsLength; c++) + { + var constructorArgument = constructorArguments[c]; + var attributeName = attribute.AttributeConstructor?.Parameters[c].Name; + sb.AddComment($"[{i}] AttributeConstructor Parameters {c+1} Name", attributeName); + sb.AddComment($"[{i}] ConstructorArgument {c+1} Kind", constructorArgument.Kind); + sb.AddComment($"[{i}] ConstructorArgument {c+1} Type", constructorArgument.Type?.Name); + sb.AddComment($"[{i}] ConstructorArgument {c+1} Value", constructorArgument.Value); + + includeSummary = false; + includeAttributeContextAttributes = true; + } + + var namedArguments = attribute.NamedArguments; + var namedArgumentsLength = namedArguments.Length; + sb.AddComment($"[{i}] NamedArguments Count", namedArgumentsLength); + for (var n = 0; n < namedArgumentsLength; n++) + { + var kvp = namedArguments[n]; + var argName = kvp.Key; + var namedArgument = kvp.Value; + sb.AddComment($"[{i}] NamedArgument {n+1} Key", argName); + sb.AddComment($"[{i}] NamedArgument {n+1} Value Kind", namedArgument.Kind); + sb.AddComment($"[{i}] NamedArgument {n+1} Value Type", namedArgument.Type?.Name); + sb.AddComment($"[{i}] NamedArgument {n+1} Value Value", namedArgument.Value); + if (capture) + { + includeSummary = false; + + var argumentValue = namedArgument.Value is true; + switch (argName) + { + case "IncludeAll": + includeAll = argumentValue; + break; + case "IncludeAttributeContextTargetSymbol": + includeAttributeContextTargetSymbol |= argumentValue; + break; + case "IncludeAttributeContextTypeSymbol": + includeAttributeContextTypeSymbol |= argumentValue; + break; + case "IncludeAttributeContextNamedTypeSymbol": + includeAttributeContextNamedTypeSymbol |= argumentValue; + break; + case "IncludeAttributeContextTargetNode": + includeAttributeContextTargetNode |= argumentValue; + break; + case "IncludeAttributeContextAttributes": + includeAttributeContextAttributes |= argumentValue; + break; + case "IncludeAttributeContextAllAttributes": + includeAttributeContextAllAttributes |= argumentValue; + break; + case "IncludeGlobalOptions": + includeGlobalOptions |= argumentValue; + break; + case "IncludeCompilation": + includeCompilation |= argumentValue; + break; + case "IncludeCompilationOptions": + includeCompilationOptions |= argumentValue; + break; + case "IncludeCompilationAssembly": + includeCompilationAssembly |= argumentValue; + break; + case "IncludeCompilationReferences": + includeCompilationReferences |= argumentValue; + break; + case "IncludeParseOptions": + includeParseOptions |= argumentValue; + break; + case "IncludeAdditionalTexts": + includeAdditionalTexts |= argumentValue; + break; + case "IncludeAdditionalTextsOptions": + includeAdditionalTextsOptions |= argumentValue; + break; + case "IncludeMetadataReferences": + includeMetadataReferences |= argumentValue; + break; + default: + includeAttributeContextAttributes = true; + break; + } + } + } + } + + return ( + includeSummary, + includeAll, + includeAttributeContextTargetSymbol, + includeAttributeContextTypeSymbol, + includeAttributeContextNamedTypeSymbol, + includeAttributeContextTargetNode, + includeAttributeContextAttributes, + includeAttributeContextAllAttributes, + includeGlobalOptions, + includeCompilation, + includeCompilationOptions, + includeCompilationAssembly, + includeCompilationReferences, + includeParseOptions, + includeAdditionalTexts, + includeAdditionalTextsOptions, + includeMetadataReferences); + } + + private static string GetRecordStructOrClass(ITypeSymbol typeSymbol) + { + if (typeSymbol.IsRecord && typeSymbol.IsReferenceType) + return "record"; + if (typeSymbol.IsRecord) + return "record struct"; + if (typeSymbol.TypeKind == TypeKind.Interface) + return "interface"; + if (typeSymbol.IsReferenceType) + return "class"; + return "struct"; + } + + public static bool Predicate(SyntaxNode syntaxNode, CancellationToken token) => true; //syntaxNode is TypeDeclarationSyntax, + + public static AttributeContext Transform(GeneratorAttributeSyntaxContext generatorAttributeSyntaxContext, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return new AttributeContext(generatorAttributeSyntaxContext); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/CodeGenerator.cs b/SourceGeneratorContext/CodeGenerator.cs new file mode 100644 index 0000000..7ea8893 --- /dev/null +++ b/SourceGeneratorContext/CodeGenerator.cs @@ -0,0 +1,318 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct CodeGenerator +{ + public CodeGenerator( + in GeneratorSourceData source, + in CancellationToken cancellationToken) + { + _attributeContext = source.Core.AttributeOptionsCompilationAndParseOptions.AttributeOptionsAndCompilation.AttributeAndOptions.AttributeContext; + _globalOptionsDescription = source.Core.AttributeOptionsCompilationAndParseOptions.AttributeOptionsAndCompilation.AttributeAndOptions.Options; + _compilationDescription = source.Core.AttributeOptionsCompilationAndParseOptions.AttributeOptionsAndCompilation.Compilation; + _parseOptionsDescription = source.Core.AttributeOptionsCompilationAndParseOptions.ParseOptions; + _additionalTextDescriptions = source.Core.AdditionalTexts; + _metadataReferenceDescriptions = source.MetadataReferences; + _cancellationToken = cancellationToken; + _buffer = new StringBuilder(); + } + + private readonly AttributeContext _attributeContext; + private readonly AnalyzerConfigOptionsDescription _globalOptionsDescription; + private readonly CompilationDescription _compilationDescription; + private readonly ParseOptionsDescription _parseOptionsDescription; + private readonly ImmutableArray _additionalTextDescriptions; + private readonly ImmutableArray _metadataReferenceDescriptions; + private readonly CancellationToken _cancellationToken; + + private readonly StringBuilder _buffer; + + public string GenerateSource() + { + _cancellationToken.ThrowIfCancellationRequested(); + _buffer.Clear(); + AutoGeneratedComment(); + StartNamespace(); + var indentLevel = ParentClasses(); + ClassDocComments(indentLevel); + PartialTypeDeclaration(indentLevel); + AppendStartClass(indentLevel); + AppendEndClass(indentLevel); + EndParentClasses(); + AppendDiagnosticLogs(); + return _buffer.ToString(); + } + + private void AutoGeneratedComment() + { + _buffer.AppendLine(Templates.AutoGeneratedComment); + } + + private void StartNamespace() + { + if (_attributeContext.ContainingNamespaceIsGlobalNamespace) return; + + _buffer.Append("namespace "); + _buffer.Append(_attributeContext.ContainingNamespaceDisplayString); + _buffer.Append(';').AppendLine(); + } + + private int ParentClasses() + { + var indentLevel = 0; + if (_attributeContext.HasParentClasses) + { + indentLevel = StartParentClasses(); + } + return indentLevel; + } + + private int StartParentClasses() + { + var indent = 0; + foreach (var parentClass in _attributeContext.ParentClasses) + { + _buffer.Append(' ', indent); + + // Use the parent's actual accessibility + var accessibilityModifier = GetAccessibility(parentClass.Accessibility); + // Include static modifier if the parent class is static + var staticModifier = parentClass.IsStatic ? "static " : ""; + var genericTypes = parentClass.TypeParameters.Any() + ? $"<{string.Join(",", parentClass.TypeParameters)}>" + : string.Empty; + _buffer.AppendLine($"{accessibilityModifier}{staticModifier}partial {parentClass.RecordStructOrClass} {parentClass.Name}{genericTypes}"); + + _buffer.Append(' ', indent); + _buffer.AppendLine("{"); + indent += 4; + } + return indent; + } + + private void ClassDocComments(int indent = 0) + { + var indentString = StringForIndent(indent); + _buffer.AppendFormat(Templates.ClassDocCommentsBegin, indentString); + + if (_attributeContext.IncludeAttributeContextTargetSymbol || _attributeContext.IncludeAll || _attributeContext.IncludeSummary) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext TargetSymbol"); + AppendIndentedLines(indent, _attributeContext.TargetSymbolDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if ((_attributeContext.IncludeAttributeContextTypeSymbol || _attributeContext.IncludeAll) && !string.IsNullOrEmpty(_attributeContext.TypeSymbolDocComments)) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext TargetSymbol as ITypeSymbol"); + AppendIndentedLines(indent,_attributeContext.TypeSymbolDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if ((_attributeContext.IncludeAttributeContextNamedTypeSymbol || _attributeContext.IncludeAll) && !string.IsNullOrEmpty(_attributeContext.NamedTypeSymbolDocComments)) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext TargetSymbol as INamedTypeSymbol"); + AppendIndentedLines(indent, _attributeContext.NamedTypeSymbolDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeAttributeContextTargetNode || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext TargetNode"); + AppendIndentedLines(indent, _attributeContext.TargetNodeDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeAttributeContextAttributes || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext Attributes"); + AppendIndentedLines(indent, _attributeContext.AttributesDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeAttributeContextAllAttributes || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "GeneratorAttributeSyntaxContext TargetSymbol AllAttributes()"); + AppendIndentedLines(indent, _attributeContext.AllAttributesDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeGlobalOptions || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "AnalyzerConfigOptionsProvider GlobalOptions"); + AppendIndentedLines(indent, _globalOptionsDescription.DocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeCompilation || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Compilation"); + AppendIndentedLines(indent, _compilationDescription.DocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeCompilationOptions || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Compilation Options"); + AppendIndentedLines(indent, _compilationDescription.OptionsDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeCompilationAssembly || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Compilation Assembly"); + AppendIndentedLines(indent, _compilationDescription.AssemblyDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeCompilationReferences || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Compilation References"); + AppendIndentedLines(indent, _compilationDescription.ReferencesDocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeParseOptions || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Parse Options"); + AppendIndentedLines(indent, _parseOptionsDescription.DocComments); + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeAdditionalTexts || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Additional Texts"); + // todo: indent + _buffer.AddComment("Number of Additional Texts", _additionalTextDescriptions.Length); + foreach (var additionalTextDescription in _additionalTextDescriptions) + { + AppendIndentedLines(indent, additionalTextDescription.DocComments); + if (_attributeContext.IncludeAdditionalTextsOptions) + { + AppendIndentedLines(indent, additionalTextDescription.OptionsComments); + } + } + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + if (_attributeContext.IncludeMetadataReferences || _attributeContext.IncludeAll) + { + _buffer.AppendFormat(Templates.ClassDocCommentsSectionBegin, indentString, "Metadata References"); + // todo: indent + _buffer.AddComment("Number of Metadata References", _metadataReferenceDescriptions.Length); + foreach (var metadataReferenceDescription in _metadataReferenceDescriptions) + { + AppendIndentedLines(indent, metadataReferenceDescription.DocComments); + } + _buffer.AppendFormat(Templates.ClassDocCommentsSectionEnd, indentString); + } + + _buffer.AppendFormat(Templates.ClassDocCommentsEnd, indentString); + } + + private void PartialTypeDeclaration(int indent = 0) + { + var genericTypes = _attributeContext.TypeParameters.Any() + ? $"<{string.Join(",", _attributeContext.TypeParameters)}>" + : string.Empty; + _buffer.Append(' ', indent); + _buffer.AppendFormat( + "{0}{1}partial {2} {3}{4}", + GetAccessibility(), + GetStatic(), + _attributeContext.RecordStructOrClass, + _attributeContext.Name, + genericTypes + ).AppendLine(); + } + + private string GetAccessibility() + { + var accessibility = _attributeContext.DeclaredAccessibility; + return GetAccessibility(accessibility); + } + + private static string GetAccessibility(Accessibility accessibility) + { + return accessibility switch + { + Accessibility.Private => "private ", + Accessibility.ProtectedAndInternal => "private protected ", + Accessibility.Protected => "protected ", + Accessibility.Internal => "internal ", + Accessibility.ProtectedOrInternal => "protected internal ", + Accessibility.Public => "public ", + _ => throw new ArgumentOutOfRangeException(nameof(accessibility), accessibility, null) + }; + } + + private string GetStatic() => _attributeContext.IsStatic ? "static " : ""; + + private void AppendStartClass(int indent = 0) + { + var indentString = StringForIndent(indent); + _buffer.Append(indentString); + _buffer.AppendLine("{"); + } + + + private void AppendEndClass(int indent = 0) + { + var indentString = StringForIndent(indent); + _buffer.Append(indentString); + _buffer.AppendLine("}"); + } + + private void EndParentClasses() + { + // Close parent classes if any + if (_attributeContext.HasParentClasses) + { + var indent = (_attributeContext.ParentClasses.Length - 1) * 4; + for (int i = 0; i < _attributeContext.ParentClasses.Length; i++) + { + AppendEndClass(indent); + indent -= 4; + } + } + } + + private void AppendDiagnosticLogs() + { + _buffer.AppendLine(); + _buffer.AppendLine("/* Diagnostic Log"); + LightweightTrace.Add(TrackingNames.DiagnosticLog_Written); + LightweightTrace.GetTrace(_buffer, TrackingNames.TracingNames); + _buffer.AppendLine("*/"); + } + + private static readonly Dictionary IndentationCache = new(); + private string StringForIndent(int indent) + { + if (!IndentationCache.TryGetValue(indent, out var indentString)) + { + indentString = new string(' ', indent); + IndentationCache[indent] = indentString; + } + return indentString; + } + + private void AppendIndentedLines(int indent, string lines) + { + var indentString = StringForIndent(indent); + bool includeLineBreak = false; + foreach (var line in lines.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (includeLineBreak) + { + _buffer.AppendLine(); + } + _buffer.Append(indentString); + _buffer.Append(line); + includeLineBreak = true; + } + } +} diff --git a/SourceGeneratorContext/CompilationDescription.cs b/SourceGeneratorContext/CompilationDescription.cs new file mode 100644 index 0000000..66b21c0 --- /dev/null +++ b/SourceGeneratorContext/CompilationDescription.cs @@ -0,0 +1,137 @@ +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct CompilationDescription +{ + public readonly string DocComments; + public readonly string OptionsDocComments; + public readonly string AssemblyDocComments; + public readonly string ReferencesDocComments; + + public CompilationDescription(Compilation compilation) + { + var sb = new StringBuilder(); + sb.AddComment("AssemblyName", compilation.AssemblyName); + sb.AddComment("Language", compilation.Language); + sb.AddComment("IsCaseSensitive", compilation.IsCaseSensitive); + sb.AddComment("DynamicType", compilation.DynamicType); + sb.AddComment("GlobalNamespace", compilation.GlobalNamespace); + sb.AddComment("ObjectType", compilation.ObjectType); + sb.AddComment("ScriptClass", compilation.ScriptClass); + sb.AddComment("SourceModule", compilation.SourceModule); + sb.AddComment("ScriptCompilationInfo", compilation.ScriptCompilationInfo); + sb.AddComment("SyntaxTrees Count", compilation.SyntaxTrees.Count()); + + DocComments = sb.ToString(); + + sb.Clear(); + var options = compilation.Options; + sb.AddComment("Language", options.Language); + sb.AddComment("OutputKind", options.OutputKind); + sb.AddComment("ModuleName", options.ModuleName); + sb.AddComment("MainTypeName", options.MainTypeName); + sb.AddComment("ScriptClassName", options.ScriptClassName); + sb.AddComment("CryptoKeyContainer", options.CryptoKeyContainer); + sb.AddComment("CryptoKeyFile", options.CryptoKeyFile); + sb.AddComment("CryptoPublicKey Length", options.CryptoPublicKey.Length); + sb.AddComment("DelaySign", options.DelaySign); + sb.AddComment("CheckOverflow", options.CheckOverflow); + sb.AddComment("Platform", options.Platform); + sb.AddComment("GeneralDiagnosticOption", options.GeneralDiagnosticOption); + sb.AddComment("WarningLevel", options.WarningLevel); + sb.AddComment("ReportSuppressedDiagnostics", options.ReportSuppressedDiagnostics); + sb.AddComment("OptimizationLevel", options.OptimizationLevel); + sb.AddComment("ConcurrentBuild", options.ConcurrentBuild); + sb.AddComment("Deterministic", options.Deterministic); + //sb.AddComment("XmlReferenceResolver", options.XmlReferenceResolver); + //sb.AddComment("SourceReferenceResolver", options.SourceReferenceResolver); + //sb.AddComment("SyntaxTreeOptionsProvider", options.SyntaxTreeOptionsProvider); + //sb.AddComment("MetadataReferenceResolver", options.MetadataReferenceResolver); + //sb.AddComment("StrongNameProvider", options.StrongNameProvider); + //sb.AddComment("AssemblyIdentityComparer", options.AssemblyIdentityComparer); + sb.AddComment("MetadataImportOptions", options.MetadataImportOptions); + sb.AddComment("PublicSign", options.PublicSign); + sb.AddComment("NullableContextOptions", options.NullableContextOptions); + sb.AddComment("SpecificDiagnosticOptions Count", options.SpecificDiagnosticOptions.Count); + foreach (var kvp in options.SpecificDiagnosticOptions) + { + sb.AddComment($"SpecificDiagnosticOptions '{kvp.Key}'", kvp.Value); + } + OptionsDocComments = sb.ToString(); + + sb.Clear(); + var assembly = compilation.Assembly; + sb.AddComment("Identity Name", assembly.Identity.Name); + sb.AddComment("Identity Version", assembly.Identity.Version); + sb.AddComment("Identity CultureName", assembly.Identity.CultureName); + sb.AddComment("Identity Flags", assembly.Identity.Flags); + sb.AddComment("Identity ContentType", assembly.Identity.ContentType); + sb.AddComment("Identity HasPublicKey", assembly.Identity.HasPublicKey); + sb.AddComment("Identity PublicKey Length", assembly.Identity.PublicKey.Length); + //sb.AddComment("Identity PublicKeyToken", string.Join(",", assembly.Identity.PublicKeyToken)); + sb.AddComment("Identity IsStrongName", assembly.Identity.IsStrongName); + sb.AddComment("Identity IsRetargetable", assembly.Identity.IsRetargetable); + sb.AddComment("IsInteractive", assembly.IsInteractive); + //sb.AddComment("GlobalNamespace", assembly.GlobalNamespace); + sb.AddComment("Modules Count", assembly.Modules.Count()); + sb.AddComment("TypeNames Count", assembly.TypeNames.Count); + sb.AddComment("NamespaceNames Count", assembly.NamespaceNames.Count); + sb.AddComment("MightContainExtensionMethods", assembly.MightContainExtensionMethods); + //sb.AddComment("Kind", assembly.Kind); + sb.AddComment("Language", assembly.Language); + sb.AddComment("Name", assembly.Name); + sb.AddComment("MetadataName", assembly.MetadataName); + sb.AddComment("MetadataToken", assembly.MetadataToken); + //sb.AddComment("IsDefinition", assembly.IsDefinition); + //sb.AddComment("IsStatic", assembly.IsStatic); + //sb.AddComment("IsVirtual", assembly.IsVirtual); + //sb.AddComment("IsOverride", assembly.IsOverride); + //sb.AddComment("IsAbstract", assembly.IsAbstract); + //sb.AddComment("IsSealed", assembly.IsSealed); + //sb.AddComment("IsExtern", assembly.IsExtern); + //sb.AddComment("IsImplicitlyDeclared", assembly.IsImplicitlyDeclared); + //sb.AddComment("CanBeReferencedByName", assembly.CanBeReferencedByName); + sb.AddComment("Locations Length", assembly.Locations.Length); + //sb.AddComment("DeclaringSyntaxReferences Length", assembly.DeclaringSyntaxReferences.Length); + sb.AddComment("DeclaredAccessibility", assembly.DeclaredAccessibility); + //sb.AddComment("OriginalDefinition", assembly.OriginalDefinition); + AssemblyDocComments = sb.ToString(); + + sb.Clear(); + sb.AddComment("References Count", compilation.References.Count()); + sb.AddComment("DirectiveReferences Count", compilation.DirectiveReferences.Count()); + sb.AddComment("ExternalReferences Count", compilation.ExternalReferences.Count()); + sb.AddComment("ReferencedAssemblyNames Count", compilation.ReferencedAssemblyNames.Count()); + //foreach (var reference in compilation.References) + // { + // sb.AppendFormat(Templates.OptionsLine, "Display", reference.Display); + // sb.AppendFormat(Templates.OptionsLine, "Properties.EmbedInteropTypes", reference.Properties.EmbedInteropTypes); + // sb.AppendFormat(Templates.OptionsLine, "Properties.Kind", reference.Properties.Kind); + // foreach (var alias in reference.Properties.Aliases) + // { + // sb.AppendFormat(Templates.OptionsLine, "Alias", alias); + // } + // } + ReferencesDocComments = sb.ToString(); + + // sb.Clear(); + // foreach (var syntaxTree in compilation.SyntaxTrees) + // { + // sb.AppendFormat(Templates.OptionsLine, "FilePath", syntaxTree.FilePath); + // sb.AppendFormat(Templates.OptionsLine, "Length", syntaxTree.Length); + // sb.AppendFormat(Templates.OptionsLine, "Encoding", syntaxTree.Encoding); + // sb.AppendFormat(Templates.OptionsLine, "HasCompilationUnitRoot", syntaxTree.HasCompilationUnitRoot); + // } + // SyntaxTreesDocComments = sb.ToString(); + } + + public static CompilationDescription Select(Compilation compilation, CancellationToken token) + { + LightweightTrace.Add(TrackingNames.CompilationDescription_Select); + + token.ThrowIfCancellationRequested(); + return new CompilationDescription(compilation); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/DocCommentDescriptionExtensions.cs b/SourceGeneratorContext/DocCommentDescriptionExtensions.cs new file mode 100644 index 0000000..001b5b3 --- /dev/null +++ b/SourceGeneratorContext/DocCommentDescriptionExtensions.cs @@ -0,0 +1,13 @@ +using System.Text; + +namespace Datacute.SourceGeneratorContext; + +public static class DocCommentDescriptionExtensions +{ + public static void AddComment(this StringBuilder sb, string propertyName, object? value) + { + if (value == null) return; + var valueString = value.ToString(); + sb.AppendFormat(Templates.OptionsLine, propertyName, Templates.EscapeStringForDocComments(valueString)); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/EquatableImmutableArray.cs b/SourceGeneratorContext/EquatableImmutableArray.cs new file mode 100644 index 0000000..4b01eb9 --- /dev/null +++ b/SourceGeneratorContext/EquatableImmutableArray.cs @@ -0,0 +1,211 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Datacute.SourceGeneratorContext; + +public sealed class EquatableImmutableArray : IEquatable>, IReadOnlyList + where T : IEquatable +{ + public static EquatableImmutableArray Empty { get; } = new(ImmutableArray.Empty); + + // The source generation pipelines compare these a lot + // so being able to quickly tell when they are different + // is important. + // We will use an instance cache to find when we can reuse + // an existing object, massively speeding up the Equals call. + #region Instance Cache + + // Thread-safe cache using dictionary of hash code -> list of arrays with that hash + private static readonly ConcurrentDictionary>>> InstanceCache = new(); + + // Static factory method with singleton handling + public static EquatableImmutableArray Create(ImmutableArray values, CancellationToken cancellationToken = default) + { + if (values.IsEmpty) + return Empty; + + // Calculate hash code for the values + var hash = CalculateHashCode(values); + + // Try to find an existing instance with the same hash and values + if (InstanceCache.TryGetValue(hash, out var list)) + { + cancellationToken.ThrowIfCancellationRequested(); + + lock (list) // Thread safety for the list + { + for (int i = list.Count - 1; i >= 0; i--) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (list[i].TryGetTarget(out var existing)) + { + // Element-by-element comparison for arrays with the same hash + if (ValuesEqual(values, existing._values)) + return existing; + } + else + { + // Remove dead references + list.RemoveAt(i); + } + } + } + } + + // Create new instance and add to cache + var result = new EquatableImmutableArray(values, hash); + + InstanceCache.AddOrUpdate(hash, + _ => new List>> { new(result) }, + (_, existingList) => + { + cancellationToken.ThrowIfCancellationRequested(); + lock (existingList) + { + existingList.Add(new WeakReference>(result)); + } + return existingList; + }); + + return result; + } + + private static bool ValuesEqual(ImmutableArray a, ImmutableArray b) + { + // Identical arrays reference check + if (a == b) return true; + + int length = a.Length; + if (length != b.Length) return false; + + var comparer = EqualityComparer.Default; + for (int i = 0; i < length; i++) + { + if (!comparer.Equals(a[i], b[i])) + return false; + } + + return true; + } + + private static int CalculateHashCode(ImmutableArray values) + { + var comparer = EqualityComparer.Default; + var hash = 0; + for (var index = 0; index < values.Length; index++) + { + var value = values[index]; + hash = HashHelpers_Combine(hash, value is null ? 0 : comparer.GetHashCode(value)); + } + return hash; + } + + #endregion + + private readonly ImmutableArray _values; + private readonly int _hashCode; + private readonly int _length; + public T this[int index] => _values[index]; + public int Count => _length; + + private EquatableImmutableArray(ImmutableArray values) + { + _values = values; + _length = values.Length; + _hashCode = CalculateHashCode(values); + } + + private EquatableImmutableArray(ImmutableArray values, int hashCode) + { + _values = values; + _length = values.Length; + _hashCode = hashCode; + } + + public bool Equals(EquatableImmutableArray? other) + { + // Fast reference equality check + if (ReferenceEquals(this, other)) return true; + + if (other is null) return false; + + // If hash codes are different, arrays can't be equal + if (_hashCode != other._hashCode) + return false; + + // We're really unlikely to get here, as we're using an instance cache + // so we've probably encountered a hash collision + + // Compare array lengths + if (_length != other._length) return false; + + // If both are empty, they're equal + if (_length == 0) return true; + + // Element-by-element comparison + var comparer = EqualityComparer.Default; + for (int i = 0; i < _length; i++) + { + if (!comparer.Equals(_values[i], other._values[i])) + return false; + } + + return true; + } + + public override bool Equals(object? obj) => obj is EquatableImmutableArray other && Equals(other); + + public override int GetHashCode() => _hashCode; + + private static int HashHelpers_Combine(int h1, int h2) + { + // RyuJIT optimizes this to use the ROL instruction + // Related GitHub pull request: https://github.com/dotnet/coreclr/pull/1830 + uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; + } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); +} + +public static class EquatableImmutableArrayExtensions +{ + public static EquatableImmutableArray ToEquatableImmutableArray(this ImmutableArray values, Func selector, CancellationToken ct = default) where T : IEquatable + { + var builder = ImmutableArray.CreateBuilder(values.Length); + foreach (TSource value in values) + { + builder.Add(selector(value)); + } + return EquatableImmutableArray.Create(builder.MoveToImmutable(), ct); + } + public static EquatableImmutableArray ToEquatableImmutableArray(this EquatableImmutableArray values, Func selector, CancellationToken ct = default) where TSource : IEquatable where T : IEquatable + { + var builder = ImmutableArray.CreateBuilder(values.Count); + foreach (TSource value in values) + { + builder.Add(selector(value)); + } + return EquatableImmutableArray.Create(builder.MoveToImmutable(), ct); + } + public static EquatableImmutableArray ToEquatableImmutableArray(this IEnumerable values, CancellationToken ct = default) where T : IEquatable => EquatableImmutableArray.Create(values.ToImmutableArray(), ct); + + public static EquatableImmutableArray ToEquatableImmutableArray(this ImmutableArray values, CancellationToken ct = default) where T : IEquatable => EquatableImmutableArray.Create(values, ct); + + public static IncrementalValuesProvider<(TLeft Left, EquatableImmutableArray Right)> CombineEquatable( + this IncrementalValuesProvider provider1, + IncrementalValuesProvider provider2) + where TRight : IEquatable + => provider1.Combine(provider2.Collect().Select(EquatableImmutableArray.Create)); + + public static IncrementalValueProvider<(TLeft Left, EquatableImmutableArray Right)> CombineEquatable( + this IncrementalValueProvider provider1, + IncrementalValuesProvider provider2) + where TRight : IEquatable + => provider1.Combine(provider2.Collect().Select(EquatableImmutableArray.Create)); + +} diff --git a/SourceGeneratorContext/Generator.cs b/SourceGeneratorContext/Generator.cs new file mode 100644 index 0000000..98cc02d --- /dev/null +++ b/SourceGeneratorContext/Generator.cs @@ -0,0 +1,69 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Datacute.SourceGeneratorContext; + +// Record structs to replace complex tuple types +public record struct AttributeAndOptions(AttributeContext AttributeContext, AnalyzerConfigOptionsDescription Options); +public record struct AttributeOptionsAndCompilation(AttributeAndOptions AttributeAndOptions, CompilationDescription Compilation); +public record struct AttributeOptionsCompilationAndParseOptions(AttributeOptionsAndCompilation AttributeOptionsAndCompilation, ParseOptionsDescription ParseOptions); +public record struct AttributeOptionsCompilationParseAndAdditionalTexts(AttributeOptionsCompilationAndParseOptions AttributeOptionsCompilationAndParseOptions, ImmutableArray AdditionalTexts); +public record struct GeneratorSourceData(AttributeOptionsCompilationParseAndAdditionalTexts Core, ImmutableArray MetadataReferences); + +[Generator(LanguageNames.CSharp)] +public sealed class Generator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + LightweightTrace.Add(TrackingNames.Generator_Initialize); + + var attributeContexts = + context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName: Templates.AttributeFullyQualified, + predicate: AttributeContext.Predicate, + transform: AttributeContext.Transform) + .WithTrackingName(TrackingNames.InitialExtraction); + + var globalOptionsDescriptionValueProvider = context.AnalyzerConfigOptionsProvider.Select(AnalyzerConfigOptionsDescription.Select); + var compilationDescriptionValueProvider = context.CompilationProvider.Select(CompilationDescription.Select); + var parseOptionsDescriptionValueProvider = context.ParseOptionsProvider.Select(ParseOptionsDescription.Select); + + // It is possible to get config options FOR each additional text, but showing them is very repetitive + var additionalTextDescriptionsValuesProvider = context.AdditionalTextsProvider.Combine(context.AnalyzerConfigOptionsProvider).Select(AdditionalTextDescription.Select); + // var additionalTextDescriptionsValuesProvider = context.AdditionalTextsProvider.Select(AdditionalTextDescription.Select); + var metadataReferenceDescriptionsValuesProvider = context.MetadataReferencesProvider.Select(MetadataReferenceDescription.Select); + + var source = attributeContexts + .Combine(globalOptionsDescriptionValueProvider) + .Select((x, _) => new AttributeAndOptions(x.Left, x.Right)) + .Combine(compilationDescriptionValueProvider) + .Select((x, _) => new AttributeOptionsAndCompilation(x.Left, x.Right)) + .Combine(parseOptionsDescriptionValueProvider) + .Select((x, _) => new AttributeOptionsCompilationAndParseOptions(x.Left, x.Right)) + .Combine(additionalTextDescriptionsValuesProvider.Collect()) + .Select((x, _) => new AttributeOptionsCompilationParseAndAdditionalTexts(x.Left, x.Right)) + .Combine(metadataReferenceDescriptionsValuesProvider.Collect()) + .Select((x, _) => new GeneratorSourceData(x.Left, x.Right)) + .WithTrackingName(TrackingNames.Combine); + + context.RegisterSourceOutput(source, Action); + } + + private void Action(SourceProductionContext sourceProductionContext, GeneratorSourceData source) + { + LightweightTrace.Add(TrackingNames.Generator_Action); + + var attributeContext = source.Core.AttributeOptionsCompilationAndParseOptions.AttributeOptionsAndCompilation.AttributeAndOptions.AttributeContext; + + var cancellationToken = sourceProductionContext.CancellationToken; + cancellationToken.ThrowIfCancellationRequested(); + + var codeGenerator = new CodeGenerator(source, cancellationToken); + + var hintName = attributeContext.DisplayString.GetHintName(); + var generatedSource = codeGenerator.GenerateSource(); + sourceProductionContext.AddSource(hintName, generatedSource); + } + + +} \ No newline at end of file diff --git a/SourceGeneratorContext/LightweightTracing/LightweightTrace.cs b/SourceGeneratorContext/LightweightTracing/LightweightTrace.cs new file mode 100644 index 0000000..12eb47b --- /dev/null +++ b/SourceGeneratorContext/LightweightTracing/LightweightTrace.cs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Stephen Denne + * https://github.com/datacute/LightweightTracing + */ + +using System.Diagnostics; +using System.Text; + +namespace Datacute.SourceGeneratorContext; + +public static class LightweightTrace +{ + private const int Capacity = 1024; + + private static readonly DateTime StartTime = DateTime.UtcNow; + private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); + + private static readonly (long, int)[] Events = new (long, int)[Capacity]; + private static int _index; + + public static void Add(int eventId) + { + Events[_index] = (Stopwatch.ElapsedTicks, eventId); + _index = (_index + 1) % Capacity; + } + + public static void GetTrace(StringBuilder stringBuilder, Dictionary eventNameMap) + { + var index = _index; + for (var i = 0; i < Capacity; i++) + { + var (timestamp, eventId) = Events[index]; + if (timestamp > 0) + { + stringBuilder.AppendFormat("{0:o} [{1:000}] {2}", + StartTime.AddTicks(timestamp), + eventId, + eventNameMap.TryGetValue(eventId, out var name) ? name : string.Empty) + .AppendLine(); + } + + index = (index + 1) % Capacity; + } + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/MetadataReferenceDescription.cs b/SourceGeneratorContext/MetadataReferenceDescription.cs new file mode 100644 index 0000000..a20a932 --- /dev/null +++ b/SourceGeneratorContext/MetadataReferenceDescription.cs @@ -0,0 +1,27 @@ +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct MetadataReferenceDescription +{ + public readonly string DocComments; + public MetadataReferenceDescription(MetadataReference metadataReference) + { + var sb = new StringBuilder(); + sb.AddComment("Display", metadataReference.Display); + //sb.AddComment("Properties Kind", metadataReference.Properties.Kind); + //sb.AddComment("Properties EmbedInteropTypes", metadataReference.Properties.EmbedInteropTypes); + //sb.AddComment("Properties Aliases Length", metadataReference.Properties.Aliases.Length); + + DocComments = sb.ToString(); + } + + public static MetadataReferenceDescription Select(MetadataReference metadataReference, CancellationToken token) + { + //LightweightTrace.Add(TrackingNames.MetadataReferenceDescription_Select); + + token.ThrowIfCancellationRequested(); + return new MetadataReferenceDescription(metadataReference); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/NameGenerators.cs b/SourceGeneratorContext/NameGenerators.cs new file mode 100644 index 0000000..fbd7ddd --- /dev/null +++ b/SourceGeneratorContext/NameGenerators.cs @@ -0,0 +1,7 @@ +namespace Datacute.SourceGeneratorContext; + +internal static class NameGenerators +{ + public static string GetHintName(this string typeDisplayName) => + $"{typeDisplayName.Replace('<', '_').Replace('>', '_')}.g.cs"; +} \ No newline at end of file diff --git a/SourceGeneratorContext/ParseOptionsDescription.cs b/SourceGeneratorContext/ParseOptionsDescription.cs new file mode 100644 index 0000000..8f90729 --- /dev/null +++ b/SourceGeneratorContext/ParseOptionsDescription.cs @@ -0,0 +1,40 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Datacute.SourceGeneratorContext; + +public readonly struct ParseOptionsDescription +{ + public readonly string DocComments; + + public ParseOptionsDescription(ParseOptions parseOptions) + { + var sb = new StringBuilder(); + sb.AddComment("Kind", parseOptions.Kind); + sb.AddComment("SpecifiedKind", parseOptions.SpecifiedKind); + sb.AddComment("DocumentationMode", parseOptions.DocumentationMode); + sb.AddComment("Language", parseOptions.Language); + if (parseOptions is CSharpParseOptions cSharpParseOptions) + { + sb.AddComment("CSharp SpecifiedLanguageVersion", cSharpParseOptions.SpecifiedLanguageVersion); + sb.AddComment("CSharp LanguageVersion", cSharpParseOptions.LanguageVersion); + } + sb.AddComment("Features Count", parseOptions.Features.Count); + sb.AddComment("PreprocessorSymbolNames Count", parseOptions.PreprocessorSymbolNames.Count()); + foreach (var preprocessorSymbolName in parseOptions.PreprocessorSymbolNames) + { + sb.AddComment($"PreprocessorSymbolName", preprocessorSymbolName); + } + + DocComments = sb.ToString(); + } + + public static ParseOptionsDescription Select(ParseOptions parseOptions, CancellationToken token) + { + LightweightTrace.Add(TrackingNames.ParseOptionsDescription_Select); + + token.ThrowIfCancellationRequested(); + return new ParseOptionsDescription(parseOptions); + } +} \ No newline at end of file diff --git a/SourceGeneratorContext/SourceGeneratorContext.csproj b/SourceGeneratorContext/SourceGeneratorContext.csproj new file mode 100644 index 0000000..c3b5395 --- /dev/null +++ b/SourceGeneratorContext/SourceGeneratorContext.csproj @@ -0,0 +1,83 @@ + + + + netstandard2.0 + 10.0 + enable + enable + true + Datacute.SourceGeneratorContext + Datacute.SourceGeneratorContext + + + + true + + + + false + + true + true + true + + + + Datacute.SourceGeneratorContext + Datacute Source Generator Context + A source generator to help creators of source generators, by creating doc-comments showing the available generation context. + SourceGenerator + README.md + false + MIT + + + + + + + $(NoWarn);NU5128 + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SourceGeneratorContext/Templates.cs b/SourceGeneratorContext/Templates.cs new file mode 100644 index 0000000..3b58285 --- /dev/null +++ b/SourceGeneratorContext/Templates.cs @@ -0,0 +1,46 @@ +namespace Datacute.SourceGeneratorContext; + +internal static class Templates +{ + private const string GeneratorNamespace = "Datacute.SourceGeneratorContext"; + private const string AttributeName = "SourceGeneratorContextAttribute"; + public const string AttributeFullyQualified = GeneratorNamespace + "." + AttributeName; + + public const string AutoGeneratedComment = /* language=c# */ + @"//------------------------------------------------------------------------------ +// +// This code was generated by the Datacute.SourceGeneratorContext. +// +//------------------------------------------------------------------------------ +"; + + public const string ClassDocCommentsBegin = /* language=c# */ + @"{0}/// +{0}/// When this item is examined by a source generator...

+"; + + public const string ClassDocCommentsSectionBegin = /* language=c# */ + @"{0}/// {1} contains: +{0}/// +{0}/// KeyValue +"; + + public const string OptionsLine = /* language=c# */ + @"/// {0}{1} +"; + + public const string ClassDocCommentsSectionEnd = /* language=c# */ + @"{0}///
+"; + + public const string ClassDocCommentsEnd = /* language=c# */ + @"{0}///
+"; + + public static string EscapeStringForDocComments(string input) => + input.Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'"); +} \ No newline at end of file diff --git a/SourceGeneratorContext/TrackingNames.cs b/SourceGeneratorContext/TrackingNames.cs new file mode 100644 index 0000000..0512364 --- /dev/null +++ b/SourceGeneratorContext/TrackingNames.cs @@ -0,0 +1,30 @@ +namespace Datacute.SourceGeneratorContext; + +public static class TrackingNames +{ + public const string InitialExtraction = nameof(InitialExtraction); + public const string Combine = nameof(Combine); + + public const int Generator_Initialize = 0; + public const int AttributeContext_Transform = 1; // too noisy to include + public const int AnalyzerConfigOptionsDescription_Select = 2; + public const int CompilationDescription_Select = 3; + public const int ParseOptionsDescription_Select = 4; + public const int AdditionalTextDescription_Select = 5; + public const int MetadataReferenceDescription_Select = 6; + public const int Generator_Action = 7; + public const int DiagnosticLog_Written = 8; + + public static readonly Dictionary TracingNames = new() + { + { Generator_Initialize, nameof(Generator_Initialize) }, + { AttributeContext_Transform, nameof(AttributeContext_Transform) }, + { AnalyzerConfigOptionsDescription_Select, nameof(AnalyzerConfigOptionsDescription_Select) }, + { CompilationDescription_Select, nameof(CompilationDescription_Select) }, + { ParseOptionsDescription_Select, nameof(ParseOptionsDescription_Select) }, + { AdditionalTextDescription_Select, nameof(AdditionalTextDescription_Select) }, + { MetadataReferenceDescription_Select, nameof(MetadataReferenceDescription_Select) }, + { Generator_Action, nameof(Generator_Action) }, + { DiagnosticLog_Written, nameof(DiagnosticLog_Written) }, + }; +} \ No newline at end of file diff --git a/SourceGeneratorContextExample/Example Text File.txt b/SourceGeneratorContextExample/Example Text File.txt new file mode 100644 index 0000000..e727da2 --- /dev/null +++ b/SourceGeneratorContextExample/Example Text File.txt @@ -0,0 +1 @@ +This file include a UTF-8 BOM \ No newline at end of file diff --git a/SourceGeneratorContextExample/Program.cs b/SourceGeneratorContextExample/Program.cs new file mode 100644 index 0000000..4c2cd52 --- /dev/null +++ b/SourceGeneratorContextExample/Program.cs @@ -0,0 +1,190 @@ +using Datacute.SourceGeneratorContext; + +Console.WriteLine("View the doc-comments for the items with the [SourceGeneratorContext] attribute"); + +// In order to demonstrate various scenarios, we're creating a lot of unused code, so... +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedType.Local +// ReSharper disable RedundantExtendsListEntry +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable NotAccessedPositionalProperty.Global +// ReSharper disable UnusedTypeParameter +#pragma warning disable CA1050 // Declare types in namespaces + +#region Simple example + +// class needs to be partial to allow for the source generator to extend it (adding doc comments in this case) +[SourceGeneratorContext] +public static partial class ClassInGlobalNamespace; + +#endregion + +#region All 'include' options + +[SourceGeneratorContext(IncludeAttributeContextTargetSymbol = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextTargetSymbol; + +[SourceGeneratorContext(IncludeAttributeContextTypeSymbol = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextTypeSymbol; + +[SourceGeneratorContext(IncludeAttributeContextNamedTypeSymbol = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextNamedTypeSymbol; + +[SourceGeneratorContext(IncludeAttributeContextTargetNode = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextTargetNode; + +[SourceGeneratorContext(IncludeAttributeContextAttributes = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextAttributes; + +[SourceGeneratorContext(IncludeAttributeContextAllAttributes = true)] +public partial class ViewClassDocsToSeeGeneratorAttributeSyntaxContextAllAttributes; + +[SourceGeneratorContext(IncludeGlobalOptions = true)] +public partial class ViewClassDocsToSeeAnalyzerConfigOptionsProviderGlobalOptions; + +[SourceGeneratorContext(IncludeCompilation = true)] +public partial class ViewClassDocsToSeeCompilation; + +[SourceGeneratorContext(IncludeCompilationOptions = true)] +public partial class ViewClassDocsToSeeCompilationOptions; + +[SourceGeneratorContext(IncludeCompilationAssembly = true)] +public partial class ViewClassDocsToSeeCompilationAssembly; + +[SourceGeneratorContext(IncludeCompilationReferences = true)] +public partial class ViewClassDocsToSeeCompilationReferences; + +[SourceGeneratorContext(IncludeParseOptions = true)] +public partial class ViewClassDocsToSeeParseOptions; + +[SourceGeneratorContext(IncludeAdditionalTexts = true)] +public partial class ViewClassDocsToSeeAdditionalTexts; + +[SourceGeneratorContext(IncludeMetadataReferences = true)] +public partial class ViewClassDocsToSeeMetadataReferences; + +// IncludeAdditionalTextsOptions seems really repetitive, so is not included in "includeAll" +[SourceGeneratorContext(IncludeAdditionalTexts = true, IncludeAdditionalTextsOptions = true)] +public partial class ViewClassDocsToSeeAdditionalTextsWithOptions; + +[SourceGeneratorContext(IncludeAll = true)] +public partial class ViewClassDocsToSeeAllAvailableDetails; + +#endregion + +#region Inner classes + +[SourceGeneratorContext] +public partial class ParentClassInGlobalNamespace: IFormattable +{ + public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty; + + public override string ToString() => string.Empty; + + [SourceGeneratorContext] + public static partial class InnerStaticClassWithinParentClass + { + [SourceGeneratorContext] + private static partial class InnerClassWithinMultipleParents; + + [SourceGeneratorContext] + internal partial class InnerNonStaticClassWithinMultipleParents; + } + + // parent class needs to be partial to allow the child classes to be partial + internal partial class InnerNonStaticClassWithinParentClass + { + [SourceGeneratorContext] + private static partial class InnerClassWithinMultipleParents; + + [SourceGeneratorContext] + private partial class InnerNonStaticClassWithinMultipleParents; + } +} + +#endregion + +#region Inheritance scenarios + +// Visual Studio only shows these below comments, not the generated comments. +// Jetbrains Rider shows both the generated comments then the below comments. +/// +/// This is an example of doc comments on both the user written class, and the generated class. +/// +/// Example remarks +[SourceGeneratorContext] +public partial class ClassWithOwnDocComments; + +[SourceGeneratorContext] +public partial class SubclassExample : ParentClassInGlobalNamespace; + +// Uncommenting this attribute on a repeated class will break the source generation, due to a duplicate hint +// The source generator is supposed to be able to handle repeated hint values, +// so perhaps this is IDE and/or build tool dependent +//[SourceGeneratorContext] +public partial class SubclassExample: ParentClassInGlobalNamespace; + +// even though the attribute is inherited, +// the generator will still only generate the doc-comments for the class with the attribute +// this is due to the implementation of the context.SyntaxProvider.ForAttributeWithMetadataName method +public partial class OverriddenClassWithoutAttribute: ParentClassInGlobalNamespace; + +// This inheritdoc DOES NOT show the doc-comments from the generated ClassWithOwnDocComments, +// but only the doc-comments from ClassWithOwnDocComments above +/// +public partial class OverriddenClassDemonstratingInheritDocWithoutGeneratedComments: ClassWithOwnDocComments; + +// This inheritdoc DOES show the doc-comments from ParentClassInGlobalNamespace, +// because ParentClassInGlobalNamespace does not have its own docs above. +// Note though that the generated comments are for the context of the attribute on the ParentClassInGlobalNamespace class +// not the context for this class +/// +public partial class OverriddenClassDemonstratingInheritDocWithGeneratedComments : ParentClassInGlobalNamespace; + +#endregion + +#region Various example scenarios + +namespace ExampleNamespace +{ + [SourceGeneratorContext] + internal static partial class ClassInNamespace; + + [SourceGeneratorContext(IncludeAttributeContextTypeSymbol = true)] + internal partial record RecordClass(string Example) + { + [SourceGeneratorContext] + internal partial class InnerClassWithinRecord; + } + + [SourceGeneratorContext] + public static partial class ParentClass + { + [SourceGeneratorContext] + internal static partial class InnerClassWithinParentClass + { + [Obsolete("Included in `TargetSymbol.AllAttributes()`")] + [SourceGeneratorContext("Included in `Attributes` and `TargetSymbol.AllAttributes()`")] + [SourceGeneratorContext] + private static partial class InnerClassWithinMultipleParents; + } + + [SourceGeneratorContext(exampleOptionalParameter: "Example of a supplied parameter")] + internal partial class InnerGenericClass + { + [SourceGeneratorContext(ExampleNamedParameter = "Example of a named parameter")] + internal partial class InnerClassOfGenericClass; + } + } +} + +[SourceGeneratorContext(IncludeAttributeContextTypeSymbol = true)] +public partial struct ExampleStructure; + +[SourceGeneratorContext(IncludeAttributeContextTypeSymbol = true)] +public partial interface IExampleInterface; + + +#endregion + +#pragma warning restore CA1050 // Declare types in namespaces diff --git a/SourceGeneratorContextExample/README.md b/SourceGeneratorContextExample/README.md new file mode 100644 index 0000000..e1d5489 --- /dev/null +++ b/SourceGeneratorContextExample/README.md @@ -0,0 +1,4 @@ +# Example project + +This project demonstrates how the `[SourceGeneratorContext]` can be added to partial classes, +to produce doc comments listing a huge amount of information about the class and compilation. diff --git a/SourceGeneratorContextExample/SecondClass.cs b/SourceGeneratorContextExample/SecondClass.cs new file mode 100644 index 0000000..9336a0f --- /dev/null +++ b/SourceGeneratorContextExample/SecondClass.cs @@ -0,0 +1,9 @@ +using Datacute.SourceGeneratorContext; + +namespace ExampleNamespace; + +[SourceGeneratorContext] +public partial class SecondClass +{ + +} \ No newline at end of file diff --git a/SourceGeneratorContextExample/SourceGeneratorContextExample.csproj b/SourceGeneratorContextExample/SourceGeneratorContextExample.csproj new file mode 100644 index 0000000..5de5785 --- /dev/null +++ b/SourceGeneratorContextExample/SourceGeneratorContextExample.csproj @@ -0,0 +1,41 @@ + + + + enable + enable + Exe + Datacute.SourceGeneratorContextExample + Datacute.SourceGeneratorContextExample + en-NZ + preview + net9.0 + + + + DemoSourceGeneratorPropertyValue + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SourceGeneratorContextExample/SourceGeneratorContextExample.sln b/SourceGeneratorContextExample/SourceGeneratorContextExample.sln new file mode 100644 index 0000000..c5fea54 --- /dev/null +++ b/SourceGeneratorContextExample/SourceGeneratorContextExample.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratorContextExample", "SourceGeneratorContextExample.csproj", "{1464453B-83DE-1F1D-58FB-2B9196BCE4C9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1464453B-83DE-1F1D-58FB-2B9196BCE4C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1464453B-83DE-1F1D-58FB-2B9196BCE4C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1464453B-83DE-1F1D-58FB-2B9196BCE4C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1464453B-83DE-1F1D-58FB-2B9196BCE4C9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F587DC78-4E3B-459B-A138-C16B2B7A314D} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4fd385 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/version.props b/version.props new file mode 100644 index 0000000..ce771c6 --- /dev/null +++ b/version.props @@ -0,0 +1,6 @@ + + + 0.0.1 + alpha + + \ No newline at end of file