diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..6fecb38 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,66 @@ +name: .NET + +on: + push: + branches: [ main, dev, feature/*, fix/*, release/* ] + + pull_request: + branches: [ main ] + + release: + types: [ published ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 9.0.x + + # Create Local NuGet Source + + - name: Create Local NuGet Directory + run: mkdir ~/nuget + + - name: Add Local Nuget Source + run: dotnet nuget add source ~/nuget + + # CodeAnalysis.Extensions + + - name: Restore CodeAnalysis.Extensions + run: dotnet restore ./src/*/CodeAnalysis.Extensions.csproj + + - name: Build CodeAnalysis.Extensions + run: dotnet build ./src/*/CodeAnalysis.Extensions.csproj --no-restore -c Release + + - name: Pack CodeAnalysis.Extensions + run: dotnet pack ./src/*/CodeAnalysis.Extensions.csproj --no-restore -o ~/nuget -c Release + + # CodeAnalysis.SourceBuilder + + - name: Restore CodeAnalysis.SourceBuilder + run: dotnet restore ./src/*/CodeAnalysis.SourceBuilder.csproj + + - name: Build CodeAnalysis.SourceBuilder + run: dotnet build ./src/*/CodeAnalysis.SourceBuilder.csproj --no-restore -c Release + + - name: Pack CodeAnalysis.SourceBuilder + run: dotnet pack ./src/*/CodeAnalysis.SourceBuilder.csproj --no-restore -o ~/nuget -c Release + + # Push + + - name: Push Packages + if: ${{ github.event_name == 'release' }} + run: > + dotnet nuget push "../../../nuget/*.nupkg" + -s https://api.nuget.org/v3/index.json + -k ${{ secrets.NuGetSourcePassword }} + --skip-duplicate diff --git a/CodeAnalysis.sln b/CodeAnalysis.sln new file mode 100644 index 0000000..8b290b0 --- /dev/null +++ b/CodeAnalysis.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeAnalysis.Extensions", "src\CodeAnalysis.Extensions\CodeAnalysis.Extensions.csproj", "{B6C02DFB-D60E-4199-B880-F4CB6037B428}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeAnalysis.SourceBuilder", "src\CodeAnalysis.SourceBuilder\CodeAnalysis.SourceBuilder.csproj", "{AFF0B5A4-3339-4CA8-B875-0D66CB784005}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6C02DFB-D60E-4199-B880-F4CB6037B428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6C02DFB-D60E-4199-B880-F4CB6037B428}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6C02DFB-D60E-4199-B880-F4CB6037B428}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6C02DFB-D60E-4199-B880-F4CB6037B428}.Release|Any CPU.Build.0 = Release|Any CPU + {AFF0B5A4-3339-4CA8-B875-0D66CB784005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFF0B5A4-3339-4CA8-B875-0D66CB784005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFF0B5A4-3339-4CA8-B875-0D66CB784005}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFF0B5A4-3339-4CA8-B875-0D66CB784005}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index c4b7717..162aa22 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# early-infra-codeanalysis -EarlyFuncPack Infra.CodeAnalysis is an infrastructure library for .NET for use in building source generators +# early-codeanalysis +EarlyFuncPack CodeAnalysis is a library for .NET for use in building source generators diff --git a/src/CodeAnalysis.Extensions/CodeAnalysis.Extensions.csproj b/src/CodeAnalysis.Extensions/CodeAnalysis.Extensions.csproj new file mode 100644 index 0000000..e6811d4 --- /dev/null +++ b/src/CodeAnalysis.Extensions/CodeAnalysis.Extensions.csproj @@ -0,0 +1,45 @@ + + + + netstandard2.0 + latest + true + disable + enable + true + true + $(NoWarn);IDE0130;IDE0290 + true + LICENSE + README.md + https://github.com/pfpack/early-codeanalysis + https://github.com/pfpack/early-codeanalysis + pfpack + Andrei Sergeev, Pavel Moskovoy + Copyright © 2025 Andrei Sergeev, Pavel Moskovoy + EarlyFuncPack CodeAnalysis is a library for .NET for use in building source generators. + PrimeFuncPack + EarlyFuncPack.CodeAnalysis.Extensions + 0.0.1 + + + + + True + + + + True + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/CodeAnalysisExtensions.cs b/src/CodeAnalysis.Extensions/Extensions/CodeAnalysisExtensions.cs new file mode 100644 index 0000000..f19b4a4 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/CodeAnalysisExtensions.cs @@ -0,0 +1,23 @@ +namespace PrimeFuncPack; + +public static partial class CodeAnalysisExtensions +{ + private const string SystemNamespace = "System"; + + private const string SystemTextJsonSerializationNamespace = "System.Text.Json.Serialization"; + + private static string InnerWithCamelCase(this string source) + { + if (string.IsNullOrEmpty(source)) + { + return string.Empty; + } + + if (source.Length is 1) + { + return source.ToLowerInvariant(); + } + + return source[0].ToString().ToLowerInvariant() + source.Substring(1); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.AsStringSourceCodeOr.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.AsStringSourceCodeOr.cs new file mode 100644 index 0000000..bcff3b7 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.AsStringSourceCodeOr.cs @@ -0,0 +1,18 @@ +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static string AsStringSourceCodeOr(this string? source, string defaultSourceCode = "\"\"") + => + string.IsNullOrEmpty(source) ? defaultSourceCode : InnerWrapStringSourceCode(source!); + + public static string AsStringSourceCodeOrStringEmpty(this string? source) + => + string.IsNullOrEmpty(source) ? "string.Empty" : InnerWrapStringSourceCode(source!); + + private static string InnerWrapStringSourceCode(string source) + { + var encodedString = source.Replace("\"", "\\\""); + return $"\"{encodedString}\""; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetConstructorArgumentValue.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetConstructorArgumentValue.cs new file mode 100644 index 0000000..a558255 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetConstructorArgumentValue.cs @@ -0,0 +1,20 @@ +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static T? GetConstructorArgumentValue(this AttributeData attributeData, int constructorOrder) + => + (T?)attributeData.InnerGetConstructorArgumentValue(constructorOrder); + + private static object? InnerGetConstructorArgumentValue(this AttributeData? attributeData, int constructorOrder) + { + if (attributeData?.ConstructorArguments.Length <= constructorOrder) + { + return default; + } + + return attributeData?.ConstructorArguments[constructorOrder].Value; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetNamedArgumentValue.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetNamedArgumentValue.cs new file mode 100644 index 0000000..914561d --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.Attribute.GetNamedArgumentValue.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static T? GetNamedArgumentValue(this AttributeData attributeData, string propertyName) + => + (T?)attributeData.InnerGetNamedArgumentValue(propertyName); + + private static object? InnerGetNamedArgumentValue(this AttributeData? attributeData, string propertyName) + { + return attributeData?.NamedArguments.FirstOrDefault(IsNameMatched).Value.Value; + + bool IsNameMatched(KeyValuePair pair) + => + string.Equals(pair.Key, propertyName, StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetCollectionType.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetCollectionType.cs new file mode 100644 index 0000000..1f91144 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetCollectionType.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static ITypeSymbol? GetCollectionTypeOrDefault(this ITypeSymbol typeSymbol) + { + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { + return arrayTypeSymbol.ElementType; + } + + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) + { + return null; + } + + var enumerableInterface = namedTypeSymbol.AllInterfaces.FirstOrDefault(IsGenericEnumerable); + if (enumerableInterface is not null) + { + return enumerableInterface.TypeArguments[0]; + } + + return typeSymbol.GetMembers().OfType().Where(IsEnumeratorMethod).Select(GetEnumeratorType).FirstOrDefault(NotNull); + + static bool IsGenericEnumerable(INamedTypeSymbol symbol) + => + InnerIsType(symbol, "System.Collections.Generic", "IEnumerable") && symbol.TypeArguments.Length is 1; + + static bool IsEnumeratorMethod(IMethodSymbol methodSymbol) + => + methodSymbol.IsGenericMethod is false && + methodSymbol.Parameters.Length is 0 && + string.Equals(methodSymbol.Name, "GetEnumerator", StringComparison.InvariantCulture); + + static ITypeSymbol? GetEnumeratorType(IMethodSymbol methodSymbol) + => + methodSymbol.ReturnType?.InnerGetEnumeratorTypeOrDefault(); + + static bool NotNull(ITypeSymbol? typeSymbol) + => + typeSymbol is not null; + } + + private static ITypeSymbol? InnerGetEnumeratorTypeOrDefault(this ITypeSymbol typeSymbol) + { + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) + { + return null; + } + + var enumeratorInterface = namedTypeSymbol.AllInterfaces.FirstOrDefault(IsGenericEnumerator); + if (enumeratorInterface is not null) + { + return enumeratorInterface.TypeArguments[0]; + } + + if (namedTypeSymbol.GetMembers().OfType().Any(IsMoveNextMethod) is false) + { + return null; + } + + return namedTypeSymbol.GetMembers().OfType().FirstOrDefault(IsCurrentProperty)?.Type; + + static bool IsGenericEnumerator(INamedTypeSymbol symbol) + => + InnerIsType(symbol, "System.Collections.Generic", "IEnumerator") && symbol.TypeArguments.Length is 1; + + static bool IsMoveNextMethod(IMethodSymbol methodSymbol) + => + methodSymbol.IsGenericMethod is false && + methodSymbol.Parameters.Length is 0 && + methodSymbol.ReturnType.InnerIsType(SystemNamespace, "Boolean") && + string.Equals(methodSymbol.Name, "MoveNext", StringComparison.InvariantCulture); + + static bool IsCurrentProperty(IPropertySymbol propertySymbol) + => + string.Equals(propertySymbol.Name, "Current", StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetDisplayedData.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetDisplayedData.cs new file mode 100644 index 0000000..c6ab7a9 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetDisplayedData.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static DisplayedTypeData GetDisplayedData(this ITypeSymbol typeSymbol, bool withNullableSymbol = false) + => + InnerGetDisplayedData( + typeSymbol: typeSymbol ?? throw new ArgumentNullException(nameof(typeSymbol)), + withNullableSymbol: withNullableSymbol); + + private static DisplayedTypeData InnerGetDisplayedData(ITypeSymbol typeSymbol, bool withNullableSymbol) + { + var symbol = typeSymbol; + var nullableSymbol = string.Empty; + + if (typeSymbol.InnerGetNullableBaseType() is ITypeSymbol baseTypeSymbol) + { + symbol = baseTypeSymbol; + if (withNullableSymbol) + { + nullableSymbol = "?"; + } + } + + if (symbol is IArrayTypeSymbol arrayTypeSymbol) + { + var elementTypeData = InnerGetChildrenDisplayedData(arrayTypeSymbol.ElementType); + + List elementTypeNameParts = [elementTypeData.DisplayedTypeName]; + elementTypeNameParts.AddRange(Enumerable.Repeat("[]", arrayTypeSymbol.Rank)); + + return new DisplayedTypeData( + allNamespaces: elementTypeData.AllNamespaces, + displayedTypeName: string.Concat(elementTypeNameParts) + nullableSymbol); + } + + if (symbol is not INamedTypeSymbol namedTypeSymbol || namedTypeSymbol.TypeArguments.Length is not > 0) + { + var typeNamespace = symbol.ContainingNamespace?.ToString(); + var typeNamespaces = new List(1); + + if (string.IsNullOrEmpty(typeNamespace) is false) + { + typeNamespaces.Add(typeNamespace ?? string.Empty); + } + + return new( + allNamespaces: typeNamespaces, + displayedTypeName: symbol.Name + nullableSymbol); + } + + var argumentTypes = namedTypeSymbol.TypeArguments.Select(InnerGetChildrenDisplayedData); + + return new( + allNamespaces: new List(argumentTypes.SelectMany(GetNamespaces)) + { + symbol.ContainingNamespace.ToString() + }, + displayedTypeName: $"{symbol.Name}<{string.Join(", ", argumentTypes.Select(GetName))}>{nullableSymbol}"); + + static DisplayedTypeData InnerGetChildrenDisplayedData(ITypeSymbol typeSymbol) + => + InnerGetDisplayedData(typeSymbol, true); + + static IEnumerable GetNamespaces(DisplayedTypeData typeData) + => + typeData.AllNamespaces; + + static string GetName(DisplayedTypeData typeData) + => + typeData.DisplayedTypeName; + } + + private static ITypeSymbol? InnerGetNullableBaseType(this ITypeSymbol typeSymbol) + { + if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) + { + return null; + } + + if (namedTypeSymbol.IsValueType is false) + { + return namedTypeSymbol.NullableAnnotation is NullableAnnotation.Annotated ? typeSymbol : null; + } + + if (namedTypeSymbol.TypeArguments.Length is 1 && namedTypeSymbol.InnerIsType(SystemNamespace, "Nullable")) + { + return namedTypeSymbol.TypeArguments[0]; + } + + return null; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumFields.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumFields.cs new file mode 100644 index 0000000..edab470 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumFields.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static IEnumerable GetEnumFields(this ITypeSymbol typeSymbol) + { + if (typeSymbol is null) + { + return []; + } + + return typeSymbol.GetMembers().OfType().Where(IsPublic).Where(IsNameNotEmpty); + + static bool IsPublic(IFieldSymbol fieldSymbol) + => + fieldSymbol.DeclaredAccessibility is Accessibility.Public; + + static bool IsNameNotEmpty(IFieldSymbol fieldSymbol) + => + string.IsNullOrEmpty(fieldSymbol.Name) is false; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumUnderlyingType.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumUnderlyingType.cs new file mode 100644 index 0000000..75de027 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetEnumUnderlyingType.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static INamedTypeSymbol? GetEnumUnderlyingType(this ITypeSymbol typeSymbol) + => + typeSymbol is INamedTypeSymbol namedTypeSymbol ? namedTypeSymbol.EnumUnderlyingType : null; +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonProperties.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonProperties.cs new file mode 100644 index 0000000..77ce25e --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonProperties.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static IReadOnlyCollection GetJsonProperties(this ITypeSymbol typeSymbol) + { + if (typeSymbol is null) + { + return []; + } + + return typeSymbol.GetMembers().OfType().Where(IsPublic).Where(IsNotIgnored).ToArray(); + + static bool IsPublic(IPropertySymbol propertySymbol) + => + propertySymbol.DeclaredAccessibility is Accessibility.Public; + + static bool IsNotIgnored(IPropertySymbol propertySymbol) + => + propertySymbol.GetAttributes().Any(IsJsonIgnoreAttribute) is false; + + static bool IsJsonIgnoreAttribute(AttributeData attributeData) + { + if (InnerIsType(attributeData?.AttributeClass, SystemTextJsonSerializationNamespace, "JsonIgnoreAttribute") is not true) + { + return false; + } + + return attributeData.InnerGetNamedArgumentValue("Condition") is null or 1; + } + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonPropertyName.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonPropertyName.cs new file mode 100644 index 0000000..f3d9198 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.GetJsonPropertyName.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static string GetJsonPropertyName(this IPropertySymbol propertySymbol) + { + _ = propertySymbol ?? throw new ArgumentNullException(nameof(propertySymbol)); + + var jsonPropertyNameAttribute = propertySymbol.GetAttributes().FirstOrDefault(IsJsonPropertyNameAttribute); + if (jsonPropertyNameAttribute is not null) + { + var name = jsonPropertyNameAttribute.InnerGetConstructorArgumentValue(0)?.ToString(); + if (string.IsNullOrEmpty(name) is false) + { + return name!; + } + } + + return propertySymbol.Name.InnerWithCamelCase(); + + static bool IsJsonPropertyNameAttribute(AttributeData attributeData) + => + InnerIsType(attributeData.AttributeClass, "System.Text.Json.Serialization", "JsonPropertyNameAttribute"); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.IsAnyType.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.IsAnyType.cs new file mode 100644 index 0000000..8cb7abd --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.IsAnyType.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static bool IsAnyType(this ITypeSymbol typeSymbol, string @namespace, params string[] types) + { + if (typeSymbol is null || types?.Length is not > 0) + { + return false; + } + + if (string.Equals(typeSymbol.ContainingNamespace?.ToString(), @namespace, StringComparison.InvariantCulture) is false) + { + return false; + } + + return types.Any(IsEqualToType); + + bool IsEqualToType(string type) + => + string.Equals(typeSymbol.Name, type, StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/Extensions/Ext.IsType.cs b/src/CodeAnalysis.Extensions/Extensions/Ext.IsType.cs new file mode 100644 index 0000000..867a211 --- /dev/null +++ b/src/CodeAnalysis.Extensions/Extensions/Ext.IsType.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace PrimeFuncPack; + +partial class CodeAnalysisExtensions +{ + public static bool IsType(this ITypeSymbol typeSymbol, string @namespace, string typeName) + => + InnerIsType(typeSymbol, @namespace, typeName); + + private static bool InnerIsType(this ITypeSymbol? typeSymbol, string @namespace, string typeName) + => + typeSymbol is not null && + string.Equals(typeSymbol.ContainingNamespace?.ToString(), @namespace, StringComparison.InvariantCulture) && + string.Equals(typeSymbol.Name, typeName, StringComparison.InvariantCulture); +} \ No newline at end of file diff --git a/src/CodeAnalysis.Extensions/TypeData/DisplayedTypeData.cs b/src/CodeAnalysis.Extensions/TypeData/DisplayedTypeData.cs new file mode 100644 index 0000000..2aa6b38 --- /dev/null +++ b/src/CodeAnalysis.Extensions/TypeData/DisplayedTypeData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace PrimeFuncPack; + +public sealed class DisplayedTypeData +{ + public DisplayedTypeData(IReadOnlyCollection allNamespaces, string displayedTypeName) + { + AllNamespaces = allNamespaces ?? []; + DisplayedTypeName = displayedTypeName ?? string.Empty; + } + + public IReadOnlyCollection AllNamespaces { get; } + + public string DisplayedTypeName { get; } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/CodeAnalysis.SourceBuilder.csproj b/src/CodeAnalysis.SourceBuilder/CodeAnalysis.SourceBuilder.csproj new file mode 100644 index 0000000..25c2362 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/CodeAnalysis.SourceBuilder.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + latest + true + disable + enable + true + true + $(NoWarn);IDE0130;IDE0290 + true + LICENSE + README.md + https://github.com/pfpack/early-codeanalysis + https://github.com/pfpack/early-codeanalysis + pfpack + Andrei Sergeev, Pavel Moskovoy + Copyright © 2025 Andrei Sergeev, Pavel Moskovoy + EarlyFuncPack CodeAnalysis is a library for .NET for use in building source generators. + PrimeFuncPack + EarlyFuncPack.CodeAnalysis.SourceBuilder + 0.0.1 + + + + + True + + + + True + + + + + + + + + \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddAlias.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddAlias.cs new file mode 100644 index 0000000..bf41735 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddAlias.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; + +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder AddAlias(string alias) + { + if (string.IsNullOrWhiteSpace(alias)) + { + return this; + } + + if (aliases.Any(AliasEquals)) + { + return this; + } + + aliases.Add(alias); + return this; + + bool AliasEquals(string aliasValue) + => + string.Equals(aliasValue, alias, StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddUsing.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddUsing.cs new file mode 100644 index 0000000..581e4fe --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AddUsing.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; + +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder AddUsing(params string[] usings) + { + if (usings?.Length is not > 0) + { + return this; + } + + foreach (var @using in usings) + { + _ = InnerAddUsing(@using); + } + + return this; + } + + private SourceBuilder InnerAddUsing(string @using) + { + if (string.IsNullOrWhiteSpace(@using)) + { + return this; + } + + if (string.Equals(@using, @namespace, StringComparison.InvariantCulture)) + { + return this; + } + + if (@namespace.StartsWith(@using + ".", StringComparison.InvariantCulture)) + { + return this; + } + + if (usings.Any(UsingEquals)) + { + return this; + } + + usings.Add(@using); + return this; + + bool UsingEquals(string usingValue) + => + string.Equals(usingValue, @using, StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendCodeLine.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendCodeLine.cs new file mode 100644 index 0000000..2727003 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendCodeLine.cs @@ -0,0 +1,21 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder AppendCodeLine(params string[] codeLines) + { + if (codeLines?.Length is not > 0) + { + return this; + } + + var builder = this; + + foreach (var line in codeLines) + { + builder = InnerAppendLineWithTabulation(line); + } + + return builder; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendDirective.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendDirective.cs new file mode 100644 index 0000000..f0fe1e5 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendDirective.cs @@ -0,0 +1,20 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder AppendDirective(string preprocessorDirective) + { + if (string.IsNullOrWhiteSpace(preprocessorDirective)) + { + return this; + } + + if (codeBuilder.Length > 0) + { + _ = codeBuilder.AppendLine(); + } + + _ = codeBuilder.Append(preprocessorDirective); + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendEmptyLine.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendEmptyLine.cs new file mode 100644 index 0000000..082f3c4 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.AppendEmptyLine.cs @@ -0,0 +1,10 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder AppendEmptyLine() + { + _ = codeBuilder.AppendLine(); + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginArguments.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginArguments.cs new file mode 100644 index 0000000..ad3ffcb --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginArguments.cs @@ -0,0 +1,10 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder BeginArguments() + { + tabulationSize++; + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCodeBlock.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCodeBlock.cs new file mode 100644 index 0000000..5fa07ad --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCodeBlock.cs @@ -0,0 +1,12 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder BeginCodeBlock() + { + _ = InnerAppendLineWithTabulation("{"); + tabulationSize++; + + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCollectionExpression.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCollectionExpression.cs new file mode 100644 index 0000000..58ff083 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginCollectionExpression.cs @@ -0,0 +1,12 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder BeginCollectionExpression() + { + _ = InnerAppendLineWithTabulation("["); + tabulationSize++; + + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginLambda.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginLambda.cs new file mode 100644 index 0000000..3ee022a --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.BeginLambda.cs @@ -0,0 +1,10 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder BeginLambda() + { + tabulationSize++; + return InnerAppendLineWithTabulation("=>"); + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.Build.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.Build.cs new file mode 100644 index 0000000..147a93a --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.Build.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Text; + +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public string Build() + { + var builder = new StringBuilder("// Auto-generated code by PrimeFuncPack").AppendLine().Append("#nullable enable"); + + if (usings.Count > 0) + { + builder = builder.AppendLine(); + } + + foreach (var @using in usings.OrderBy(GetNamespaceOrder)) + { + builder = builder.AppendLine().Append("using").Append(' ').Append(@using).Append(';'); + } + + builder = builder.AppendLine().AppendLine().Append("namespace").Append(' ').Append(@namespace).Append(';'); + + if (aliases.Count > 0) + { + builder = builder.AppendLine(); + } + + foreach (var alias in aliases) + { + builder = builder.AppendLine().Append("using").Append(' ').Append(alias).Append(';'); + } + + if (codeBuilder.Length is not > 0) + { + return builder.ToString(); + } + + return builder.AppendLine().AppendLine().Append(codeBuilder).ToString(); + + static string GetNamespaceOrder(string @namespace) + => + @namespace.StartsWith("System", StringComparison.InvariantCulture) ? "_" + @namespace : @namespace; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndArguments.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndArguments.cs new file mode 100644 index 0000000..28afe41 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndArguments.cs @@ -0,0 +1,10 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder EndArguments() + { + tabulationSize--; + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCodeBlock.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCodeBlock.cs new file mode 100644 index 0000000..29e04a8 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCodeBlock.cs @@ -0,0 +1,17 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder EndCodeBlock(string? finalSymbol = default) + { + tabulationSize--; + _ = InnerAppendLineWithTabulation("}"); + + if (string.IsNullOrWhiteSpace(finalSymbol) is false) + { + _ = codeBuilder.Append(finalSymbol); + } + + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCollectionExpression.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCollectionExpression.cs new file mode 100644 index 0000000..2481025 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndCollectionExpression.cs @@ -0,0 +1,17 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder EndCollectionExpression(string? finalSymbol = default) + { + tabulationSize--; + _ = InnerAppendLineWithTabulation("]"); + + if (string.IsNullOrWhiteSpace(finalSymbol) is false) + { + _ = codeBuilder.Append(finalSymbol); + } + + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndLambda.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndLambda.cs new file mode 100644 index 0000000..b318c2b --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/Builder.EndLambda.cs @@ -0,0 +1,10 @@ +namespace PrimeFuncPack; + +partial class SourceBuilder +{ + public SourceBuilder EndLambda() + { + tabulationSize--; + return this; + } +} \ No newline at end of file diff --git a/src/CodeAnalysis.SourceBuilder/SourceBuilder/SourceBuilder.cs b/src/CodeAnalysis.SourceBuilder/SourceBuilder/SourceBuilder.cs new file mode 100644 index 0000000..5c846e1 --- /dev/null +++ b/src/CodeAnalysis.SourceBuilder/SourceBuilder/SourceBuilder.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Text; + +namespace PrimeFuncPack; + +public sealed partial class SourceBuilder +{ + private const int TabulationLength = 4; + + private readonly List usings = []; + + private readonly string @namespace; + + private readonly List aliases = []; + + private readonly StringBuilder codeBuilder = new(); + + private int tabulationSize = 0; + + public SourceBuilder(string? @namespace) + => + this.@namespace = string.IsNullOrWhiteSpace(@namespace) ? "PrimeFuncPack" : @namespace!; + + private SourceBuilder InnerAppendLineWithTabulation(string codeLine) + { + if (codeBuilder.Length > 0) + { + _ = codeBuilder.AppendLine(); + } + + if (tabulationSize > 0) + { + var tabulation = new string(' ', TabulationLength * tabulationSize); + _ = codeBuilder.Append(tabulation); + } + + _ = codeBuilder.Append(codeLine); + return this; + } +} \ No newline at end of file