diff --git a/README.md b/README.md index 4acaf4b..2b1f8fc 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,13 @@ This is a generator to create a class-diagram of PlantUML from the C# source cod **README.md Version revision history** -| Version | Commit | Comment | -| ------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 1.1 | [e73b4fe](https://github.com/pierre3/PlantUmlClassDiagramGenerator/commit/e73b4feed9cd261271eb990a9c859f53536e8d7c) | Add "-excludeUmlBeginEndTags" option | +| Version | Commit | Comment | +|---------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| +| 1.5 | [d92afc6](https://github.com/AnastasiaKallisto/PlantUmlClassDiagramGenerator/commit/d92afc66bfd328c229aad82e9a22b3988727a4e1) | Add "-saveFields" option | +| 1.4 | [ebc52e8](https://github.com/AnastasiaKallisto/PlantUmlClassDiagramGenerator/commit/ebc52e88f4719d28948f881de67796fc71c88cbd) | Add "-noGetSetForProperties" option | +| 1.3 | [d97e179](https://github.com/AnastasiaKallisto/PlantUmlClassDiagramGenerator/commit/d97e17911111f27906d2a5a3eeb3f8137ebdb979) | Add "-removeSystemCollectionsAssociations" option | +| 1.2 | [cd9eed2](https://github.com/AnastasiaKallisto/PlantUmlClassDiagramGenerator/commit/cd9eed2539f9bac73410e6b6f1566ce979429f6f) | Add "-addPackageTags" option | +| 1.1 | [e73b4fe](https://github.com/pierre3/PlantUmlClassDiagramGenerator/commit/e73b4feed9cd261271eb990a9c859f53536e8d7c) | Add "-excludeUmlBeginEndTags" option | | 1.0 | [70bb820](https://github.com/pierre3/PlantUmlClassDiagramGenerator/commit/70bb8202f7f489aa2d85ce9c25c58121c8f63aed) | Because the README.md for other languages is not always updated at the same time, a version number is needed | ## Roslyn Source Generator @@ -39,22 +43,26 @@ dotnet tool install --global PlantUmlClassDiagramGenerator Run the "puml-gen" command. ```bat -puml-gen InputPath [OutputPath] [-dir] [-public | -ignore IgnoreAccessibilities] [-excludePaths ExcludePathList] [-createAssociation] +puml-gen InputPath [OutputPath] [-dir] [-addPackageTags] [-public | -ignore IgnoreAccessibilities] [-excludePaths ExcludePathList] [-createAssociation] ``` -- InputPath: (Required) Sets a input source file or directory name. -- OutputPath: (Optional) Sets a output file or directory name. +- **InputPath:** (Required) Sets a input source file or directory name. +- **OutputPath:** (Optional) Sets a output file or directory name. If you omit this option, plantuml files are outputted to same directory as the input files. -- -dir: (Optional) Specify when InputPath and OutputPath are directory names. -- -public: (Optional) If specified, only public accessibility members are output. -- -ignore: (Optional) Specify the accessibility of members to ignore, with a comma separated list. -- -excludePaths: (Optional) Specify the exclude file and directory. +- **-dir**: (Optional) Specify when InputPath and OutputPath are directory names. +- **-addPackageTags:** (Optional) If there is "-dir" tag, then program adds "package" tags and puts all relations in the end of include.puml. Relations will not be shown in other files +- **-removeSystemCollectionsAssociations**: (Optional) If there are properties or fields like "IList" and other SystemCollections, there will be no relation with IList, but relation with T will be shown, if it isn't base type (string, int ...) +- **-noGetSetForProperties**: (Optional) Removes <\> and <\> for properties in classes +- **-saveFields**: (Optional) Saves all fields when -createAssociation is used. Associations will be without labels. +- **-public:** (Optional) If specified, only public accessibility members are output. +- **-ignore:** (Optional) Specify the accessibility of members to ignore, with a comma separated list. +- **-excludePaths:** (Optional) Specify the exclude file and directory. Specifies a relative path from the "InputPath", with a comma separated list. To exclude multiple paths, which contain a specific folder name, preceed the name by "\*\*/". Example: "**/bin" -- -createAssociation: (Optional) Create object associations from references of fields and properites. -- -allInOne: (Optional) Only if -dir is set: copy the output of all diagrams to file include.puml (this allows a PlanUMLServer to render it). -- -attributeRequired: (Optional) When this switch is enabled, only types with "PlantUmlDiagramAttribute" in the type declaration will be output. -- -excludeUmlBeginEndTags: (Optional) When this switch is enabled, it will exclude the \"@startuml\" and \"@enduml\" tags from the puml file. +- **-createAssociation:** (Optional) Create object associations from references of fields and properites. +- **-allInOne:** (Optional) Only if -dir is set: copy the output of all diagrams to file include.puml (this allows a PlanUMLServer to render it). +- **-attributeRequired:** (Optional) When this switch is enabled, only types with "PlantUmlDiagramAttribute" in the type declaration will be output. +- **-excludeUmlBeginEndTags:** (Optional) When this switch is enabled, it will exclude the \"@startuml\" and \"@enduml\" tags from the puml file. examples ```bat diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator.cs deleted file mode 100644 index c9ff164..0000000 --- a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator.cs +++ /dev/null @@ -1,545 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using PlantUmlClassDiagramGenerator.Attributes; - -namespace PlantUmlClassDiagramGenerator.Library; - -public class ClassDiagramGenerator( - TextWriter writer, - string indent, - Accessibilities ignoreMemberAccessibilities = Accessibilities.None, - bool createAssociation = true, - bool attributeRequired = false, - bool excludeUmlBeginEndTags = false) : CSharpSyntaxWalker -{ - private readonly HashSet types = []; - private readonly List additionalTypeDeclarationNodes = []; - private readonly Accessibilities ignoreMemberAccessibilities = ignoreMemberAccessibilities; - private readonly RelationshipCollection relationships = new(); - private readonly TextWriter writer = writer; - private readonly string indent = indent; - private int nestingDepth = 0; - private readonly bool createAssociation = createAssociation; - private readonly bool attributeRequired = attributeRequired; - private readonly bool excludeUmlBeginEndTags = excludeUmlBeginEndTags; - private readonly Dictionary escapeDictionary = new() - { - {@"(?[^{]){(?{[^{])", "${before}{${after}"}, - {@"(?[^}])}(?[^}])", "${before}}${after}"}, - }; - - public void Generate(SyntaxNode root) - { - if (!this.excludeUmlBeginEndTags) WriteLine("@startuml"); - GenerateInternal(root); - if (!this.excludeUmlBeginEndTags) WriteLine("@enduml"); - } - - public void GenerateInternal(SyntaxNode root) - { - Visit(root); - GenerateAdditionalTypeDeclarations(); - GenerateRelationships(); - } - - public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) - { - VisitTypeDeclaration(node, () => base.VisitInterfaceDeclaration(node)); - } - - public override void VisitClassDeclaration(ClassDeclarationSyntax node) - { - VisitTypeDeclaration(node, () => base.VisitClassDeclaration(node)); - } - - public override void VisitRecordDeclaration(RecordDeclarationSyntax node) - { - if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (SkipInnerTypeDeclaration(node)) { return; } - - relationships.AddInnerclassRelationFrom(node); - relationships.AddInheritanceFrom(node); - var modifiers = GetTypeModifiersText(node.Modifiers); - var abstractKeyword = (node.Modifiers.Any(SyntaxKind.AbstractKeyword) ? "abstract " : ""); - - var typeName = TypeNameText.From(node); - var name = typeName.Identifier; - var typeParam = typeName.TypeArguments; - var type = $"{name}{typeParam}"; - var typeParams = typeParam.TrimStart('<').TrimEnd('>').Split([','], StringSplitOptions.RemoveEmptyEntries); - types.Add(name); - - var typeKeyword = (node.Kind() == SyntaxKind.RecordStructDeclaration) ? "struct" : "class"; - WriteLine($"{abstractKeyword}{typeKeyword} {type} {modifiers}<> {{"); - - nestingDepth++; - var parameters = node.ParameterList?.Parameters ?? Enumerable.Empty(); - foreach (var parameter in parameters) - { - VisitRecordParameter(node, type, typeParams, parameter); - } - base.VisitRecordDeclaration(node); - nestingDepth--; - - WriteLine("}"); - } - - private void VisitRecordParameter(RecordDeclarationSyntax node, string type, string[] typeParams, ParameterSyntax parameter) - { - var parameterType = parameter.Type; - var isTypeParameterProp = typeParams.Contains(parameterType.ToString()); - var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); - if (associationAttrSyntax is not null) - { - var associationAttr = CreateAssociationAttribute(associationAttrSyntax); - relationships.AddAssociationFrom(node, parameter, associationAttr); - } - else if (!createAssociation - || parameter.AttributeLists.HasIgnoreAssociationAttribute() - || parameterType.GetType() == typeof(PredefinedTypeSyntax) - || parameterType.GetType() == typeof(NullableTypeSyntax) - || isTypeParameterProp) - { - // ParameterList-Property: always public - var parameterModifiers = "+ "; - var parameterName = parameter.Identifier.ToString(); - - // ParameterList-Property always have get and init accessor - var accessorStr = "<> <>"; - - var useLiteralInit = parameter.Default?.Value is not null; - var initValue = useLiteralInit - ? (" = " + escapeDictionary.Aggregate(parameter.Default.Value.ToString(), - (n, e) => Regex.Replace(n, e.Key, e.Value))) - : ""; - WriteLine($"{parameterModifiers}{parameterName} : {parameterType} {accessorStr}{initValue}"); - } - else - { - if (type.GetType() == typeof(GenericNameSyntax)) - { - additionalTypeDeclarationNodes.Add(parameterType); - } - relationships.AddAssociationFrom(parameter, node); - } - } - - public override void VisitStructDeclaration(StructDeclarationSyntax node) - { - if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (SkipInnerTypeDeclaration(node)) { return; } - - relationships.AddInnerclassRelationFrom(node); - relationships.AddInheritanceFrom(node); - - var typeName = TypeNameText.From(node); - var name = typeName.Identifier; - var typeParam = typeName.TypeArguments; - var type = $"{name}{typeParam}"; - - types.Add(name); - - WriteLine($"struct {type} {{"); - - nestingDepth++; - base.VisitStructDeclaration(node); - nestingDepth--; - - WriteLine("}"); - } - - public override void VisitEnumDeclaration(EnumDeclarationSyntax node) - { - if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (SkipInnerTypeDeclaration(node)) { return; } - - relationships.AddInnerclassRelationFrom(node); - - var type = $"{node.Identifier}"; - - types.Add(type); - - WriteLine($"{node.EnumKeyword} {type} {{"); - - nestingDepth++; - base.VisitEnumDeclaration(node); - nestingDepth--; - - WriteLine("}"); - } - - public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) - { - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (IsIgnoreMember(node.Modifiers)) { return; } - foreach (var parameter in node.ParameterList?.Parameters) - { - var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); - if (associationAttrSyntax is not null) - { - var associationAttr = CreateAssociationAttribute(associationAttrSyntax); - relationships.AddAssociationFrom(node, parameter, associationAttr); - } - } - var modifiers = GetMemberModifiersText(node.Modifiers, - isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); - var name = node.Identifier.ToString(); - var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); - - WriteLine($"{modifiers}{name}({string.Join(", ", args)})"); - } - - public override void VisitFieldDeclaration(FieldDeclarationSyntax node) - { - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (IsIgnoreMember(node.Modifiers)) { return; } - - var modifiers = GetMemberModifiersText(node.Modifiers, - isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); - var type = node.Declaration.Type; - var variables = node.Declaration.Variables; - var parentClass = (node.Parent as TypeDeclarationSyntax); - var isTypeParameterField = parentClass?.TypeParameterList?.Parameters - .Any(t => t.Identifier.Text == type.ToString()) ?? false; - - foreach (var field in variables) - { - Type fieldType = type.GetType(); - var associationAttrSyntax = node.AttributeLists.GetAssociationAttributeSyntax(); - if (associationAttrSyntax is not null) - { - var associationAttr = CreateAssociationAttribute(associationAttrSyntax); - relationships.AddAssociationFrom(node, associationAttr); - } - else if (!createAssociation - || node.AttributeLists.HasIgnoreAssociationAttribute() - || fieldType == typeof(PredefinedTypeSyntax) - || fieldType == typeof(NullableTypeSyntax) - || isTypeParameterField) - { - var useLiteralInit = field.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; - var initValue = useLiteralInit - ? (" = " + escapeDictionary.Aggregate(field.Initializer.Value.ToString(), - (f, e) => Regex.Replace(f, e.Key, e.Value))) - : ""; - WriteLine($"{modifiers}{field.Identifier} : {type}{initValue}"); - } - else - { - if (fieldType == typeof(GenericNameSyntax)) - { - additionalTypeDeclarationNodes.Add(type); - } - relationships.AddAssociationFrom(node, field); - } - } - } - - public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) - { - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (IsIgnoreMember(node.Modifiers)) { return; } - - var type = node.Type; - - var parentClass = (node.Parent as TypeDeclarationSyntax); - var isTypeParameterProp = parentClass?.TypeParameterList?.Parameters - .Any(t => t.Identifier.Text == type.ToString()) ?? false; - - var typeIgnoringNullable = type is NullableTypeSyntax nullableTypeSyntax ? nullableTypeSyntax.ElementType : type; - - var associationAttrSyntax = node.AttributeLists.GetAssociationAttributeSyntax(); - if (associationAttrSyntax is not null) - { - var associationAttr = CreateAssociationAttribute(associationAttrSyntax); - relationships.AddAssociationFrom(node, associationAttr); - } - else if (!createAssociation - || node.AttributeLists.HasIgnoreAssociationAttribute() - || typeIgnoringNullable is PredefinedTypeSyntax - || isTypeParameterProp) - { - var modifiers = GetMemberModifiersText(node.Modifiers, - isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); - var name = node.Identifier.ToString(); - //Property does not have an accessor is an expression-bodied property. (get only) - var accessorStr = "<>"; - if (node.AccessorList != null) - { - var accessor = node.AccessorList.Accessors - .Where(x => !x.Modifiers.Select(y => y.Kind()).Contains(SyntaxKind.PrivateKeyword)) - .Select(x => $"<<{(x.Modifiers.ToString() == "" ? "" : (x.Modifiers.ToString() + " "))}{x.Keyword}>>"); - accessorStr = string.Join(" ", accessor); - } - var useLiteralInit = node.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; - var initValue = useLiteralInit - ? (" = " + escapeDictionary.Aggregate(node.Initializer.Value.ToString(), - (n, e) => Regex.Replace(n, e.Key, e.Value))) - : ""; - - WriteLine($"{modifiers}{name} : {type} {accessorStr}{initValue}"); - } - else - { - if (type.GetType() == typeof(GenericNameSyntax)) - { - additionalTypeDeclarationNodes.Add(type); - } - relationships.AddAssociationFrom(node, typeIgnoringNullable); - } - } - - private static PlantUmlAssociationAttribute CreateAssociationAttribute(AttributeSyntax associationAttribute) - { - var attributeProps = associationAttribute.ArgumentList.Arguments.Select(arg => new - { - Name = arg.NameEquals.Name.ToString(), - Value = arg.Expression.GetLastToken().ValueText - }); - return new PlantUmlAssociationAttribute() - { - Association = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Association))?.Value, - Name = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Name))?.Value, - RootLabel = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.RootLabel))?.Value, - LeafLabel = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.LeafLabel))?.Value, - Label = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Label))?.Value - }; - } - - public override void VisitMethodDeclaration(MethodDeclarationSyntax node) - { - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (IsIgnoreMember(node.Modifiers)) { return; } - foreach (var parameter in node.ParameterList?.Parameters) - { - var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); - if (associationAttrSyntax is not null) - { - var associationAttr = CreateAssociationAttribute(associationAttrSyntax); - relationships.AddAssociationFrom(node, parameter, associationAttr); - } - } - var modifiers = GetMemberModifiersText(node.Modifiers, - isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); - var name = node.Identifier.ToString(); - var returnType = node.ReturnType.ToString(); - var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); - - WriteLine($"{modifiers}{name}({string.Join(", ", args)}) : {returnType}"); - } - - public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) - { - WriteLine($"{node.Identifier}{node.EqualsValue},"); - } - - public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) - { - if (IsIgnoreMember(node.Modifiers)) { return; } - - var modifiers = GetMemberModifiersText(node.Modifiers, - isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); - var name = string.Join(",", node.Declaration.Variables.Select(v => v.Identifier)); - var typeName = node.Declaration.Type.ToString(); - - WriteLine($"{modifiers} <<{node.EventKeyword}>> {name} : {typeName} "); - } - - public override void VisitGenericName(GenericNameSyntax node) - { - if (createAssociation) - { - additionalTypeDeclarationNodes.Add(node); - } - } - - private void WriteLine(string line) - { - var space = string.Concat(Enumerable.Repeat(indent, nestingDepth)); - writer.WriteLine(space + line); - } - - private bool SkipInnerTypeDeclaration(SyntaxNode node) - { - if (nestingDepth <= 0) return false; - - additionalTypeDeclarationNodes.Add(node); - return true; - } - - private void GenerateAdditionalTypeDeclarations() - { - for (int i = 0; i < additionalTypeDeclarationNodes.Count; i++) - { - SyntaxNode node = additionalTypeDeclarationNodes[i]; - if (node is GenericNameSyntax genericNode) - { - if (createAssociation) - { - GenerateAdditionalGenericTypeDeclaration(genericNode); - } - continue; - } - Visit(node); - } - } - - private void GenerateAdditionalGenericTypeDeclaration(GenericNameSyntax genericNode) - { - var typename = TypeNameText.From(genericNode); - if (!types.Contains(typename.Identifier)) - { - WriteLine($"class {typename.Identifier}{typename.TypeArguments} {{"); - WriteLine("}"); - types.Add(typename.Identifier); - } - } - - private void GenerateRelationships() - { - foreach (var relationship in relationships) - { - WriteLine(relationship.ToString()); - } - } - - private void VisitTypeDeclaration(TypeDeclarationSyntax node, Action visitBase) - { - if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } - if (node.AttributeLists.HasIgnoreAttribute()) { return; } - if (SkipInnerTypeDeclaration(node)) { return; } - - relationships.AddInnerclassRelationFrom(node); - relationships.AddInheritanceFrom(node); - - var modifiers = GetTypeModifiersText(node.Modifiers); - var keyword = (node.Modifiers.Any(SyntaxKind.AbstractKeyword) ? "abstract " : "") - + node.Keyword.ToString(); - - var typeName = TypeNameText.From(node); - var name = typeName.Identifier; - var typeParam = typeName.TypeArguments; - var type = $"{name}{typeParam}"; - - types.Add(name); - - WriteLine($"{keyword} {type} {modifiers}{{"); - - nestingDepth++; - visitBase(); - nestingDepth--; - - WriteLine("}"); - } - - private static string GetTypeModifiersText(SyntaxTokenList modifiers) - { - var tokens = modifiers.Select(token => - { - switch (token.Kind()) - { - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.InternalKeyword: - case SyntaxKind.AbstractKeyword: - return ""; - default: - return $"<<{token.ValueText}>>"; - } - }).Where(token => token != ""); - - var result = string.Join(" ", tokens); - if (result != string.Empty) - { - result += " "; - }; - return result; - } - - private bool IsIgnoreMember(SyntaxTokenList modifiers) - { - if (ignoreMemberAccessibilities == Accessibilities.None) { return false; } - - var tokenKinds = HasAccessModifier(modifiers) - ? modifiers.Select(x => x.Kind()).ToArray() - : [SyntaxKind.PrivateKeyword]; - - if (ignoreMemberAccessibilities.HasFlag(Accessibilities.ProtectedInternal) - && tokenKinds.Contains(SyntaxKind.ProtectedKeyword) - && tokenKinds.Contains(SyntaxKind.InternalKeyword)) - { - return true; - } - - if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Public) - && tokenKinds.Contains(SyntaxKind.PublicKeyword)) - { - return true; - } - - if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Protected) - && tokenKinds.Contains(SyntaxKind.ProtectedKeyword)) - { - return true; - } - - if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Internal) - && tokenKinds.Contains(SyntaxKind.InternalKeyword)) - { - return true; - } - - if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Private) - && tokenKinds.Contains(SyntaxKind.PrivateKeyword)) - { - return true; - } - return false; - } - - private static string GetMemberModifiersText( - SyntaxTokenList modifiers, - bool isInterfaceMember) - { - var tokens = modifiers.Select(token => - { - return token.Kind() switch - { - SyntaxKind.PublicKeyword => "+", - SyntaxKind.PrivateKeyword => "-", - SyntaxKind.ProtectedKeyword => "#", - SyntaxKind.AbstractKeyword or SyntaxKind.StaticKeyword => $"{{{token.ValueText}}}", - _ => $"<<{token.ValueText}>>", - }; - }).ToList(); - if (!isInterfaceMember && !HasAccessModifier(modifiers)) - { - tokens.Add("-"); - } - var result = string.Join(" ", tokens); - if (result != string.Empty) - { - result += " "; - }; - return result; - } - - private static bool HasAccessModifier(SyntaxTokenList modifiers) - { - return modifiers.Any(token => - token.IsKind(SyntaxKind.PublicKeyword) - || token.IsKind(SyntaxKind.PrivateKeyword) - || token.IsKind(SyntaxKind.ProtectedKeyword) - || token.IsKind(SyntaxKind.InternalKeyword)); - } -} diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/AdditionalTypeGenerator.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/AdditionalTypeGenerator.cs new file mode 100644 index 0000000..17eba29 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/AdditionalTypeGenerator.cs @@ -0,0 +1,35 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + private void GenerateAdditionalTypeDeclarations() + { + for (int i = 0; i < additionalTypeDeclarationNodes.Count; i++) + { + SyntaxNode node = additionalTypeDeclarationNodes[i]; + if (node is GenericNameSyntax genericNode) + { + if (createAssociation) + { + GenerateAdditionalGenericTypeDeclaration(genericNode); + } + continue; + } + Visit(node); + } + } + + private void GenerateAdditionalGenericTypeDeclaration(GenericNameSyntax genericNode) + { + var typename = TypeNameText.From(genericNode); + if (!types.Contains(typename.Identifier)) + { + WriteLine($"class {typename.Identifier}{typename.TypeArguments} {{"); + WriteLine("}"); + types.Add(typename.Identifier); + } + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ClassDiagramGenerator.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ClassDiagramGenerator.cs new file mode 100644 index 0000000..d75fdea --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ClassDiagramGenerator.cs @@ -0,0 +1,210 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using PlantUmlClassDiagramGenerator.Attributes; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator( + TextWriter writer, + string indent, + Accessibilities ignoreMemberAccessibilities = Accessibilities.None, + bool createAssociation = true, + bool attributeRequired = false, + bool excludeUmlBeginEndTags = false, + bool addPackageTags = false, + bool removeSystemCollectionsAssociations = false, + bool noGetSetForProperties = false, + bool saveFields = false) : CSharpSyntaxWalker +{ + private readonly HashSet types = []; + private readonly List additionalTypeDeclarationNodes = []; + private readonly Accessibilities ignoreMemberAccessibilities = ignoreMemberAccessibilities; + public readonly RelationshipCollection relationships = new(); + private readonly TextWriter writer = writer; + private readonly string indent = indent; + private int nestingDepth = 0; + private readonly bool createAssociation = createAssociation; + private readonly bool attributeRequired = attributeRequired; + private readonly bool excludeUmlBeginEndTags = excludeUmlBeginEndTags; + private readonly bool addPackageTags = addPackageTags; + private readonly bool removeSystemCollectionsAssociations = removeSystemCollectionsAssociations; + private readonly bool noGetSetForProperties = noGetSetForProperties; + private readonly bool saveFields = saveFields; + private readonly Dictionary escapeDictionary = new() + { + {@"(?[^{]){(?{[^{])", "${before}{${after}"}, + {@"(?[^}])}(?[^}])", "${before}}${after}"}, + }; + + public void Generate(SyntaxNode root) + { + if (!this.excludeUmlBeginEndTags) WriteLine("@startuml"); + GenerateInternal(root); + if (!this.excludeUmlBeginEndTags) WriteLine("@enduml"); + } + + public void GenerateInternal(SyntaxNode root) + { + Visit(root); + GenerateAdditionalTypeDeclarations(); + if (!this.addPackageTags) + GenerateRelationships(); + } + + private static PlantUmlAssociationAttribute CreateAssociationAttribute(AttributeSyntax associationAttribute) + { + var attributeProps = associationAttribute.ArgumentList.Arguments.Select(arg => new + { + Name = arg.NameEquals.Name.ToString(), + Value = arg.Expression.GetLastToken().ValueText + }).ToList(); + return new PlantUmlAssociationAttribute() + { + Association = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Association))?.Value, + Name = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Name))?.Value, + RootLabel = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.RootLabel))?.Value, + LeafLabel = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.LeafLabel))?.Value, + Label = attributeProps.FirstOrDefault(prop => prop.Name == nameof(PlantUmlAssociationAttribute.Label))?.Value + }; + } + + + + public override void VisitGenericName(GenericNameSyntax node) + { + if (createAssociation) + { + additionalTypeDeclarationNodes.Add(node); + } + } + + private void WriteLine(string line) + { + var space = string.Concat(Enumerable.Repeat(indent, nestingDepth)); + writer.WriteLine(space + line); + } + + private bool SkipInnerTypeDeclaration(SyntaxNode node) + { + if (nestingDepth <= 0) return false; + if (nestingDepth == 1 && addPackageTags) return false; + additionalTypeDeclarationNodes.Add(node); + return true; + } + + private static string GetTypeModifiersText(SyntaxTokenList modifiers) + { + var tokens = modifiers.Select(token => + { + switch (token.Kind()) + { + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.InternalKeyword: + case SyntaxKind.AbstractKeyword: + return ""; + default: + return $"<<{token.ValueText}>>"; + } + }).Where(token => token != ""); + + var result = string.Join(" ", tokens); + if (result != string.Empty) + { + result += " "; + }; + return result; + } + + private bool IsIgnoreMember(SyntaxTokenList modifiers) + { + if (ignoreMemberAccessibilities == Accessibilities.None) { return false; } + + var tokenKinds = HasAccessModifier(modifiers) + ? modifiers.Select(x => x.Kind()).ToArray() + : [SyntaxKind.PrivateKeyword]; + + if (ignoreMemberAccessibilities.HasFlag(Accessibilities.ProtectedInternal) + && tokenKinds.Contains(SyntaxKind.ProtectedKeyword) + && tokenKinds.Contains(SyntaxKind.InternalKeyword)) + { + return true; + } + + if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Public) + && tokenKinds.Contains(SyntaxKind.PublicKeyword)) + { + return true; + } + + if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Protected) + && tokenKinds.Contains(SyntaxKind.ProtectedKeyword)) + { + return true; + } + + if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Internal) + && tokenKinds.Contains(SyntaxKind.InternalKeyword)) + { + return true; + } + + if (ignoreMemberAccessibilities.HasFlag(Accessibilities.Private) + && tokenKinds.Contains(SyntaxKind.PrivateKeyword)) + { + return true; + } + return false; + } + + private static string GetMemberModifiersText( + SyntaxTokenList modifiers, + bool isInterfaceMember) + { + var tokens = modifiers.Select(token => + { + return token.Kind() switch + { + SyntaxKind.PublicKeyword => "+", + SyntaxKind.PrivateKeyword => "-", + SyntaxKind.ProtectedKeyword => "#", + SyntaxKind.AbstractKeyword or SyntaxKind.StaticKeyword => $"{{{token.ValueText}}}", + _ => $"<<{token.ValueText}>>", + }; + }).ToList(); + if (!isInterfaceMember && !HasAccessModifier(modifiers)) + { + tokens.Add("-"); + } + var result = string.Join(" ", tokens); + if (result != string.Empty) + { + result += " "; + }; + return result; + } + + private static bool HasAccessModifier(SyntaxTokenList modifiers) + { + return modifiers.Any(token => + token.IsKind(SyntaxKind.PublicKeyword) + || token.IsKind(SyntaxKind.PrivateKeyword) + || token.IsKind(SyntaxKind.ProtectedKeyword) + || token.IsKind(SyntaxKind.InternalKeyword)); + } + + private static string CapitalizeFirstLetter(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + if (input.Length == 1) + return char.ToUpper(input[0]) + ""; + + return char.ToUpper(input[0]) + input.Substring(1); + } +} diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ConstructorVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ConstructorVisitor.cs new file mode 100644 index 0000000..ef96b71 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/ConstructorVisitor.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (IsIgnoreMember(node.Modifiers)) { return; } + foreach (var parameter in node.ParameterList?.Parameters) + { + var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); + if (associationAttrSyntax is not null) + { + var associationAttr = CreateAssociationAttribute(associationAttrSyntax); + relationships.AddAssociationFrom(node, parameter, associationAttr); + } + } + var modifiers = GetMemberModifiersText(node.Modifiers, + isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); + var name = node.Identifier.ToString(); + var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); + + WriteLine($"{modifiers}{name}({string.Join(", ", args)})"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EnumVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EnumVisitor.cs new file mode 100644 index 0000000..cba15e7 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EnumVisitor.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitEnumDeclaration(EnumDeclarationSyntax node) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + relationships.AddInnerclassRelationFrom(node); + + var type = $"{node.Identifier}"; + + types.Add(type); + + WriteLine($"{node.EnumKeyword} {type} {{"); + + nestingDepth++; + base.VisitEnumDeclaration(node); + nestingDepth--; + + WriteLine("}"); + } + + public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) + { + WriteLine($"{node.Identifier}{node.EqualsValue},"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EventVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EventVisitor.cs new file mode 100644 index 0000000..b1c8cf0 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/EventVisitor.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) + { + if (IsIgnoreMember(node.Modifiers)) { return; } + + var modifiers = GetMemberModifiersText(node.Modifiers, + isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); + var name = string.Join(",", node.Declaration.Variables.Select(v => v.Identifier)); + var typeName = node.Declaration.Type.ToString(); + + WriteLine($"{modifiers} <<{node.EventKeyword}>> {name} : {typeName} "); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/FieldVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/FieldVisitor.cs new file mode 100644 index 0000000..f5840a3 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/FieldVisitor.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using PlantUmlClassDiagramGenerator.Attributes; +using PlantUmlClassDiagramGenerator.Library.Enums; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitFieldDeclaration(FieldDeclarationSyntax node) + { + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (IsIgnoreMember(node.Modifiers)) { return; } + + var modifiers = GetMemberModifiersText(node.Modifiers, + isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); + var type = node.Declaration.Type; + var variables = node.Declaration.Variables; + var parentClass = (node.Parent as TypeDeclarationSyntax); + var isTypeParameterField = parentClass?.TypeParameterList?.Parameters + .Any(t => t.Identifier.Text == type.ToString()) ?? false; + + foreach (var field in variables) + { + Type fieldType = type.GetType(); + var associationAttrSyntax = node.AttributeLists.GetAssociationAttributeSyntax(); + if (associationAttrSyntax is not null) + { + var associationAttr = CreateAssociationAttribute(associationAttrSyntax); + relationships.AddAssociationFrom(node, associationAttr); + } + else if (!createAssociation + || node.AttributeLists.HasIgnoreAssociationAttribute() + || fieldType == typeof(PredefinedTypeSyntax) + || fieldType == typeof(NullableTypeSyntax) + || isTypeParameterField) + { + FillAssociatedField(field, modifiers, type); + } + else + { + if (type.GetType() == typeof(GenericNameSyntax)) + ProcessGenericType(node, type, field, modifiers); + else if (saveFields) + { + FillAssociatedField(field, modifiers, type); + relationships.AddAssociationFromWithNoLabel(node, field); + } else + relationships.AddAssociationFrom(node, field); + } + } + } + + private void FillAssociatedField(VariableDeclaratorSyntax field, string modifiers, TypeSyntax type) + { + var useLiteralInit = field.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; + var initValue = useLiteralInit + ? (" = " + escapeDictionary.Aggregate(field.Initializer.Value.ToString(), + (f, e) => Regex.Replace(f, e.Key, e.Value))) + : ""; + WriteLine($"{modifiers}{field.Identifier} : {type}{initValue}"); + } + + private void ProcessGenericType(FieldDeclarationSyntax node, TypeSyntax type, VariableDeclaratorSyntax field, string modifiers) + { + if (this.removeSystemCollectionsAssociations) + { + ProcessWithoutSystemCollections(node, type, field, modifiers); + } + else + { + additionalTypeDeclarationNodes.Add(type); + relationships.AddAssociationFrom(node, field); + } + } + + private void ProcessWithoutSystemCollections(FieldDeclarationSyntax node, TypeSyntax type, VariableDeclaratorSyntax field, string modifiers) + { + var t = type.ToString().Split('<')[0]; + if (!Enum.TryParse(t, out SystemCollectionsTypes _)) + { + additionalTypeDeclarationNodes.Add(type); + relationships.AddAssociationFrom(node, field); + } + else + { + FillAssociatedField(field, modifiers, type); + var s = type.ToString().Split('<')[1]; + s = s.Remove(s.Length - 1); + if (!Enum.TryParse(CapitalizeFirstLetter(s), out BaseTypes _)) + relationships.AddAssociationFrom(node, new PlantUmlAssociationAttribute() + { + Association = "o--", + Name = s + }); + } + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/MethodVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/MethodVisitor.cs new file mode 100644 index 0000000..1169d6d --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/MethodVisitor.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (IsIgnoreMember(node.Modifiers)) { return; } + foreach (var parameter in node.ParameterList?.Parameters) + { + var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); + if (associationAttrSyntax is not null) + { + var associationAttr = CreateAssociationAttribute(associationAttrSyntax); + relationships.AddAssociationFrom(node, parameter, associationAttr); + } + } + var modifiers = GetMemberModifiersText(node.Modifiers, + isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); + var name = node.Identifier.ToString(); + var returnType = node.ReturnType.ToString(); + var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); + + WriteLine($"{modifiers}{name}({string.Join(", ", args)}) : {returnType}"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/NamespaceVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/NamespaceVisitor.cs new file mode 100644 index 0000000..4ebef85 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/NamespaceVisitor.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax node) + { + if (this.addPackageTags) + VisitFileScopedNamespaceDeclaration(node, () => base.VisitFileScopedNamespaceDeclaration(node)); + else + base.VisitFileScopedNamespaceDeclaration(node); + } + + public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + if (this.addPackageTags) + VisitNamespaceDeclaration(node, () => base.VisitNamespaceDeclaration(node)); + else + base.VisitNamespaceDeclaration(node); + } + + private void VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax node, Action visitBase) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + var typeName = NamespaceNameText.From(node); + + WriteLine($"package \"{typeName.Identifier}\" {{"); + nestingDepth++; + visitBase(); + nestingDepth--; + WriteLine("}"); + } + + private void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node, Action visitBase) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + var typeName = NamespaceNameText.From(node); + + WriteLine($"package \"{typeName.Identifier}\"{{"); + nestingDepth++; + visitBase(); + nestingDepth--; + WriteLine("}"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/PropertyVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/PropertyVisitor.cs new file mode 100644 index 0000000..90919cc --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/PropertyVisitor.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using PlantUmlClassDiagramGenerator.Attributes; +using PlantUmlClassDiagramGenerator.Library.Enums; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (IsIgnoreMember(node.Modifiers)) { return; } + + var type = node.Type; + + var parentClass = (node.Parent as TypeDeclarationSyntax); + var isTypeParameterProp = parentClass?.TypeParameterList?.Parameters + .Any(t => t.Identifier.Text == type.ToString()) ?? false; + + var typeIgnoringNullable = type is NullableTypeSyntax nullableTypeSyntax ? nullableTypeSyntax.ElementType : type; + + var associationAttrSyntax = node.AttributeLists.GetAssociationAttributeSyntax(); + if (associationAttrSyntax is not null) + { + var associationAttr = CreateAssociationAttribute(associationAttrSyntax); + relationships.AddAssociationFrom(node, associationAttr); + } + else if (!createAssociation + || node.AttributeLists.HasIgnoreAssociationAttribute() + || typeIgnoringNullable is PredefinedTypeSyntax + || isTypeParameterProp) + { + FillAssociatedProperty(node, type); + } + else + { + if (type.GetType() == typeof(GenericNameSyntax)) + ProcessGenericType(node, type, typeIgnoringNullable); + else if (this.saveFields) + { + FillAssociatedProperty(node, type); + relationships.AddAssociationFromWithNoLabel(node, typeIgnoringNullable); + } else + relationships.AddAssociationFrom(node, typeIgnoringNullable); + } + } + + private void ProcessGenericType(PropertyDeclarationSyntax node, TypeSyntax type, TypeSyntax typeIgnoringNullable) + { + if (this.removeSystemCollectionsAssociations) + { + ProcessWithoutSystemCollections(node, type, typeIgnoringNullable); + } + else + { + additionalTypeDeclarationNodes.Add(type); + relationships.AddAssociationFrom(node, typeIgnoringNullable); + } + } + + private void ProcessWithoutSystemCollections(PropertyDeclarationSyntax node, TypeSyntax type, TypeSyntax typeIgnoringNullable) + { + var t = node.Type.ToString().Split('<')[0]; + if (!Enum.TryParse(t, out SystemCollectionsTypes _)) + { + additionalTypeDeclarationNodes.Add(type); + relationships.AddAssociationFrom(node, typeIgnoringNullable); + } + else + { + FillAssociatedProperty(node, type); + var s = node.Type.ToString(); + s = s.Substring(s.IndexOf('<') + 1, s.LastIndexOf('>') - s.IndexOf('<') - 1); + if (!Enum.TryParse(CapitalizeFirstLetter(s), out BaseTypes _)) + relationships.AddAssociationFrom(node, new PlantUmlAssociationAttribute() + { + Association = "o--", + Name = s + }); + } + } + + private void FillAssociatedProperty(PropertyDeclarationSyntax node, TypeSyntax type) + { + var modifiers = GetMemberModifiersText(node.Modifiers, + isInterfaceMember: node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)); + var name = node.Identifier.ToString(); + //Property does not have an accessor is an expression-bodied property. (get only) + var accessorStr = "<>"; + if (node.AccessorList != null) + { + var accessor = node.AccessorList.Accessors + .Where(x => !x.Modifiers.Select(y => y.Kind()).Contains(SyntaxKind.PrivateKeyword)) + .Select(x => $"<<{(x.Modifiers.ToString() == "" ? "" : (x.Modifiers.ToString() + " "))}{x.Keyword}>>"); + accessorStr = string.Join(" ", accessor); + } + + var useLiteralInit = node.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; + var initValue = useLiteralInit + ? (" = " + escapeDictionary.Aggregate(node.Initializer.Value.ToString(), + (n, e) => Regex.Replace(n, e.Key, e.Value))) + : ""; + + if (noGetSetForProperties) + WriteLine($"{modifiers}{name} : {type} {initValue}"); + else + WriteLine($"{modifiers}{name} : {type} {accessorStr}{initValue}"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RecordVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RecordVisitor.cs new file mode 100644 index 0000000..3ea47ba --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RecordVisitor.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitRecordDeclaration(RecordDeclarationSyntax node) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + relationships.AddInnerclassRelationFrom(node); + relationships.AddInheritanceFrom(node); + var modifiers = GetTypeModifiersText(node.Modifiers); + var abstractKeyword = (node.Modifiers.Any(SyntaxKind.AbstractKeyword) ? "abstract " : ""); + + var typeName = TypeNameText.From(node); + var name = typeName.Identifier; + var typeParam = typeName.TypeArguments; + var type = $"{name}{typeParam}"; + var typeParams = typeParam.TrimStart('<').TrimEnd('>').Split([','], StringSplitOptions.RemoveEmptyEntries); + types.Add(name); + + var typeKeyword = (node.Kind() == SyntaxKind.RecordStructDeclaration) ? "struct" : "class"; + WriteLine($"{abstractKeyword}{typeKeyword} {type} {modifiers}<> {{"); + + nestingDepth++; + var parameters = node.ParameterList?.Parameters ?? Enumerable.Empty(); + foreach (var parameter in parameters) + { + VisitRecordParameter(node, type, typeParams, parameter); + } + base.VisitRecordDeclaration(node); + nestingDepth--; + + WriteLine("}"); + } + + private void VisitRecordParameter(RecordDeclarationSyntax node, string type, string[] typeParams, ParameterSyntax parameter) + { + var parameterType = parameter.Type; + var isTypeParameterProp = typeParams.Contains(parameterType.ToString()); + var associationAttrSyntax = parameter.AttributeLists.GetAssociationAttributeSyntax(); + if (associationAttrSyntax is not null) + { + var associationAttr = CreateAssociationAttribute(associationAttrSyntax); + relationships.AddAssociationFrom(node, parameter, associationAttr); + } + else if (!createAssociation + || parameter.AttributeLists.HasIgnoreAssociationAttribute() + || parameterType.GetType() == typeof(PredefinedTypeSyntax) + || parameterType.GetType() == typeof(NullableTypeSyntax) + || isTypeParameterProp) + { + // ParameterList-Property: always public + var parameterModifiers = "+ "; + var parameterName = parameter.Identifier.ToString(); + + // ParameterList-Property always have get and init accessor + var accessorStr = "<> <>"; + + var useLiteralInit = parameter.Default?.Value is not null; + var initValue = useLiteralInit + ? (" = " + escapeDictionary.Aggregate(parameter.Default.Value.ToString(), + (n, e) => Regex.Replace(n, e.Key, e.Value))) + : ""; + WriteLine($"{parameterModifiers}{parameterName} : {parameterType} {accessorStr}{initValue}"); + } + else + { + if (type.GetType() == typeof(GenericNameSyntax)) + { + additionalTypeDeclarationNodes.Add(parameterType); + } + relationships.AddAssociationFrom(parameter, node); + } + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RelationshipGenerator.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RelationshipGenerator.cs new file mode 100644 index 0000000..5beb5f3 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/RelationshipGenerator.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + private void GenerateRelationships() + { + foreach (var relationship in relationships) + { + WriteLine(relationship.ToString()); + } + } + + public static string[] GenerateRelationships(RelationshipCollection relationshipCollection) + { + List strings = new List(); + strings.AddRange(relationshipCollection.Select(r => r.ToString())); + + return strings.Distinct().ToArray(); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/StructVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/StructVisitor.cs new file mode 100644 index 0000000..0408097 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/StructVisitor.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitStructDeclaration(StructDeclarationSyntax node) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + relationships.AddInnerclassRelationFrom(node); + relationships.AddInheritanceFrom(node); + + var typeName = TypeNameText.From(node); + var name = typeName.Identifier; + var typeParam = typeName.TypeArguments; + var type = $"{name}{typeParam}"; + + types.Add(name); + + WriteLine($"struct {type} {{"); + + nestingDepth++; + base.VisitStructDeclaration(node); + nestingDepth--; + + WriteLine("}"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/TypeVisitor.cs b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/TypeVisitor.cs new file mode 100644 index 0000000..7086486 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/ClassDiagramGenerator/TypeVisitor.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +public partial class ClassDiagramGenerator +{ + public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + VisitTypeDeclaration(node, () => base.VisitInterfaceDeclaration(node)); + } + + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + VisitTypeDeclaration(node, () => base.VisitClassDeclaration(node)); + } + + private void VisitTypeDeclaration(TypeDeclarationSyntax node, Action visitBase) + { + if (attributeRequired && !node.AttributeLists.HasDiagramAttribute()) { return; } + if (node.AttributeLists.HasIgnoreAttribute()) { return; } + if (SkipInnerTypeDeclaration(node)) { return; } + + relationships.AddInnerclassRelationFrom(node); + relationships.AddInheritanceFrom(node); + + var modifiers = GetTypeModifiersText(node.Modifiers); + var keyword = (node.Modifiers.Any(SyntaxKind.AbstractKeyword) ? "abstract " : "") + + node.Keyword.ToString(); + + var typeName = TypeNameText.From(node); + var name = typeName.Identifier; + var typeParam = typeName.TypeArguments; + var type = $"{name}{typeParam}"; + + types.Add(name); + + WriteLine($"{keyword} {type} {modifiers}{{"); + + nestingDepth++; + visitBase(); + nestingDepth--; + + WriteLine("}"); + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/Enums/BaseTypes.cs b/src/PlantUmlClassDiagramGenerator.Library/Enums/BaseTypes.cs new file mode 100644 index 0000000..0053b5b --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/Enums/BaseTypes.cs @@ -0,0 +1,26 @@ +namespace PlantUmlClassDiagramGenerator.Library.Enums; + +public enum BaseTypes +{ + Int, + Int16, + Int32, + Int64, + Int128, + Long, + Short, + Byte, + Decimal, + Double, + Float, + Char, + Bool, + String, + StringBuilder, + Object, + Dynamic, + DateTime, + TimeSpan, + Guid +} + diff --git a/src/PlantUmlClassDiagramGenerator.Library/Enums/SystemCollectionsTypes.cs b/src/PlantUmlClassDiagramGenerator.Library/Enums/SystemCollectionsTypes.cs new file mode 100644 index 0000000..4b4fc14 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/Enums/SystemCollectionsTypes.cs @@ -0,0 +1,36 @@ +namespace PlantUmlClassDiagramGenerator.Library.Enums; + +public enum SystemCollectionsTypes +{ + // classes + ArrayList, + BitArray, + CaseInsensitiveComparer, + CaseInsensitiveHashCodeProvider, + CollectionBase, + Comparer, + DictionaryBase, + Hashtable, + Queue, + ReadOnlyCollectionBase, + SortedList, + Stack, + StructuralComparisons, + List, // Actually not SystemCollections but... meh + + // structs + DictionaryEntry, + + // interfaces + ICollection, + IComparer, + IDictionary, + IDictionaryEnumerator, + IEnumerable, + IEnumerator, + IEqualityComparer, + IHashCodeProvider, + IList, + IStructuralComparable, + IStructuralEquatable +} diff --git a/src/PlantUmlClassDiagramGenerator.Library/NamespaceNameText.cs b/src/PlantUmlClassDiagramGenerator.Library/NamespaceNameText.cs new file mode 100644 index 0000000..51158f1 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator.Library/NamespaceNameText.cs @@ -0,0 +1,24 @@ +namespace PlantUmlClassDiagramGenerator.Library; + +using Microsoft.CodeAnalysis.CSharp.Syntax; + +public class NamespaceNameText +{ + public string Identifier { get; set; } + + public static NamespaceNameText From(FileScopedNamespaceDeclarationSyntax syntax) + { + return new NamespaceNameText + { + Identifier = syntax.Name.ToString() + }; + } + + public static NamespaceNameText From(NamespaceDeclarationSyntax syntax) + { + return new NamespaceNameText + { + Identifier = syntax.Name.ToString() + }; + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/Relationship.cs b/src/PlantUmlClassDiagramGenerator.Library/Relationship.cs index 09363fc..9829109 100644 --- a/src/PlantUmlClassDiagramGenerator.Library/Relationship.cs +++ b/src/PlantUmlClassDiagramGenerator.Library/Relationship.cs @@ -13,4 +13,32 @@ public override string ToString() { return $"{baseTypeName.Identifier}{baseLabel} {symbol}{subLabel} {subTypeName.Identifier}{centerLabel}"; } + + private bool Equals(Relationship other) + { + return Equals(baseTypeName, other.baseTypeName) + && Equals(subTypeName, other.subTypeName) + && Equals(baseLabel, other.baseLabel) + && Equals(subLabel, other.subLabel); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Relationship)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (baseTypeName != null ? baseTypeName.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (subTypeName != null ? subTypeName.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (baseLabel != null ? baseLabel.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (subLabel != null ? subLabel.GetHashCode() : 0); + return hashCode; + } + } } \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator.Library/RelationshipCollection.cs b/src/PlantUmlClassDiagramGenerator.Library/RelationshipCollection.cs index bce54e4..cba5673 100644 --- a/src/PlantUmlClassDiagramGenerator.Library/RelationshipCollection.cs +++ b/src/PlantUmlClassDiagramGenerator.Library/RelationshipCollection.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using PlantUmlClassDiagramGenerator.Attributes; @@ -10,6 +11,14 @@ public class RelationshipCollection : IEnumerable { private readonly IList items = new List(); + public void AddAll(RelationshipCollection collection) + { + foreach (var c in collection) + { + items.Add(c); + } + } + public void AddInheritanceFrom(TypeDeclarationSyntax syntax) { if (syntax.BaseList == null) return; @@ -45,6 +54,17 @@ public void AddAssociationFrom(FieldDeclarationSyntax node, VariableDeclaratorSy var rootName = TypeNameText.From(rootNode); AddRelationship(leafName, rootName, symbol, fieldIdentifier); } + + public void AddAssociationFromWithNoLabel(FieldDeclarationSyntax node, VariableDeclaratorSyntax field) + { + if (node.Declaration.Type is not SimpleNameSyntax leafNode + || node.Parent is not BaseTypeDeclarationSyntax rootNode) return; + + var symbol = field.Initializer == null ? "-->" : "o->"; + var leafName = TypeNameText.From(leafNode); + var rootName = TypeNameText.From(rootNode); + AddRelationship(leafName, rootName, symbol, ""); + } public void AddAssociationFrom(PropertyDeclarationSyntax node, TypeSyntax typeIgnoringNullable) { @@ -57,6 +77,17 @@ public void AddAssociationFrom(PropertyDeclarationSyntax node, TypeSyntax typeIg var rootName = TypeNameText.From(rootNode); AddRelationship(leafName, rootName, symbol, nodeIdentifier); } + + public void AddAssociationFromWithNoLabel(PropertyDeclarationSyntax node, TypeSyntax typeIgnoringNullable) + { + if (typeIgnoringNullable is not SimpleNameSyntax leafNode + || node.Parent is not BaseTypeDeclarationSyntax rootNode) return; + + var symbol = node.Initializer == null ? "-->" : "o->"; + var leafName = TypeNameText.From(leafNode); + var rootName = TypeNameText.From(rootNode); + AddRelationship(leafName, rootName, symbol, ""); + } public void AddAssociationFrom(ParameterSyntax node, RecordDeclarationSyntax parent) { @@ -76,7 +107,7 @@ public void AddAssociationFrom(PropertyDeclarationSyntax node, PlantUmlAssociati var leafName = GetLeafName(attribute.Name, node.Type); if (leafName is null) { return; } var rootName = TypeNameText.From(rootNode); - AddeRationship(attribute, leafName, rootName); + AddRelationship(attribute, leafName, rootName); } @@ -86,7 +117,7 @@ public void AddAssociationFrom(MethodDeclarationSyntax node, ParameterSyntax par var leafName = GetLeafName(attribute.Name, parameter.Type); if (leafName is null) { return; } var rootName = TypeNameText.From(rootNode); - AddeRationship(attribute, leafName, rootName); + AddRelationship(attribute, leafName, rootName); } public void AddAssociationFrom(RecordDeclarationSyntax node, ParameterSyntax parameter, PlantUmlAssociationAttribute attribute) @@ -95,7 +126,7 @@ public void AddAssociationFrom(RecordDeclarationSyntax node, ParameterSyntax par var leafName = GetLeafName(attribute.Name, parameter.Type); if (leafName is null) { return; } var rootName = TypeNameText.From(rootNode); - AddeRationship(attribute, leafName, rootName); + AddRelationship(attribute, leafName, rootName); } public void AddAssociationFrom(ConstructorDeclarationSyntax node, ParameterSyntax parameter, PlantUmlAssociationAttribute attribute) @@ -104,7 +135,7 @@ public void AddAssociationFrom(ConstructorDeclarationSyntax node, ParameterSynta var leafName = GetLeafName(attribute.Name, parameter.Type); if (leafName is null) { return; } var rootName = TypeNameText.From(rootNode); - AddeRationship(attribute, leafName, rootName); + AddRelationship(attribute, leafName, rootName); } public void AddAssociationFrom(FieldDeclarationSyntax node, PlantUmlAssociationAttribute attribute) @@ -113,14 +144,14 @@ public void AddAssociationFrom(FieldDeclarationSyntax node, PlantUmlAssociationA var leafName = GetLeafName(attribute.Name, node.Declaration.Type); if(leafName is null) { return; } var rootName = TypeNameText.From(rootNode); - AddeRationship(attribute, leafName, rootName); + AddRelationship(attribute, leafName, rootName); } private static TypeNameText GetLeafName(string attributeName, TypeSyntax typeSyntax) { if (!string.IsNullOrWhiteSpace(attributeName)) { - return new TypeNameText() { Identifier = attributeName }; + return new TypeNameText() { Identifier = attributeName, TypeArguments = ""}; } else if (typeSyntax is SimpleNameSyntax simpleNode) { @@ -130,15 +161,19 @@ private static TypeNameText GetLeafName(string attributeName, TypeSyntax typeSyn } - private void AddeRationship(PlantUmlAssociationAttribute attribute, TypeNameText leafName, TypeNameText rootName) + private void AddRelationship(PlantUmlAssociationAttribute attribute, TypeNameText leafName, TypeNameText rootName) { var symbol = string.IsNullOrEmpty(attribute.Association) ? "--" : attribute.Association; - items.Add(new Relationship(rootName, leafName, symbol, attribute.RootLabel, attribute.LeafLabel, attribute.Label)); + var relationship = new Relationship(rootName, leafName, symbol, attribute.RootLabel, attribute.LeafLabel, attribute.Label); + if (!items.Contains(relationship)) + items.Add(relationship); } private void AddRelationship(TypeNameText leafName, TypeNameText rootName, string symbol, string nodeIdentifier) { - items.Add(new Relationship(rootName, leafName, symbol, "", nodeIdentifier + leafName.TypeArguments)); + var relationship = new Relationship(rootName, leafName, symbol, "", nodeIdentifier + leafName.TypeArguments); + if (!items.Contains(relationship)) + items.Add(relationship); } public IEnumerator GetEnumerator() diff --git a/src/PlantUmlClassDiagramGenerator.Library/TypeNameText.cs b/src/PlantUmlClassDiagramGenerator.Library/TypeNameText.cs index 6c66655..4c01784 100644 --- a/src/PlantUmlClassDiagramGenerator.Library/TypeNameText.cs +++ b/src/PlantUmlClassDiagramGenerator.Library/TypeNameText.cs @@ -72,4 +72,25 @@ public static TypeNameText From(BaseTypeDeclarationSyntax syntax) TypeArguments = typeArgs }; } + + private bool Equals(TypeNameText other) + { + return Identifier == other.Identifier && TypeArguments == other.TypeArguments; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TypeNameText)obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((Identifier != null ? Identifier.GetHashCode() : 0) * 397) ^ (TypeArguments != null ? TypeArguments.GetHashCode() : 0); + } + } } \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator/Generator/IPlantUmlGenerator.cs b/src/PlantUmlClassDiagramGenerator/Generator/IPlantUmlGenerator.cs new file mode 100644 index 0000000..02f9ff5 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator/Generator/IPlantUmlGenerator.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using PlantUmlClassDiagramGenerator.Library; + +namespace PlantUmlClassDiagramGenerator.Generator; + +public interface IPlantUmlGenerator +{ + public bool GeneratePlantUml(Dictionary parameters); + + public static Accessibilities GetIgnoreAccessibilities(Dictionary parameters) + { + var ignoreAcc = Accessibilities.None; + if (parameters.ContainsKey("-public")) + { + ignoreAcc = Accessibilities.Private | Accessibilities.Internal + | Accessibilities.Protected | Accessibilities.ProtectedInternal; + } + else if (parameters.TryGetValue("-ignore", out string value)) + { + var ignoreItems = value.Split(','); + foreach (var item in ignoreItems) + { + if (Enum.TryParse(item, true, out Accessibilities acc)) + { + ignoreAcc |= acc; + } + } + } + return ignoreAcc; + } + + +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromDirGenerator.cs b/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromDirGenerator.cs new file mode 100644 index 0000000..8536f51 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromDirGenerator.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using PlantUmlClassDiagramGenerator.Library; +using PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +namespace PlantUmlClassDiagramGenerator.Generator; + +public class PlantUmlFromDirGenerator: IPlantUmlGenerator +{ + public bool GeneratePlantUml(Dictionary parameters) + { + var inputRoot = parameters["in"]; + if (!Directory.Exists(inputRoot)) + { + Console.WriteLine($"Directory \"{inputRoot}\" does not exist."); + return false; + } + + // Use GetFullPath to fully support relative paths. + var outputRoot = Path.GetFullPath(inputRoot); + if (parameters.TryGetValue("out", out string outValue)) + { + outputRoot = outValue; + try + { + Directory.CreateDirectory(outputRoot); + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + var excludePaths = new List(); + var pumlexclude = PathHelper.CombinePath(inputRoot, ".pumlexclude"); + if (File.Exists(pumlexclude)) + { + excludePaths = File + .ReadAllLines(pumlexclude) + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Select(path => path.Trim()) + .ToList(); + } + if (parameters.TryGetValue("-excludePaths", out string excludePathValue)) + { + var splitOptions = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; + excludePaths.AddRange(excludePathValue.Split(',', splitOptions)); + } + + var excludeUmlBeginEndTags = parameters.ContainsKey("-excludeUmlBeginEndTags"); + var files = Directory.EnumerateFiles(inputRoot, "*.cs", SearchOption.AllDirectories); + + var includeRefs = new StringBuilder(); + if (!excludeUmlBeginEndTags) includeRefs.AppendLine("@startuml"); + + var error = false; + var filesToProcess = ExcludeFileFilter.GetFilesToProcess(files, excludePaths, inputRoot); + RelationshipCollection relationships = new(); + foreach (var inputFile in filesToProcess) + { + Console.WriteLine($"Processing \"{inputFile}\"..."); + try + { + var outputDir = PathHelper.CombinePath(outputRoot, Path.GetDirectoryName(inputFile).Replace(inputRoot, "")); + Directory.CreateDirectory(outputDir); + var outputFile = PathHelper.CombinePath(outputDir, Path.GetFileNameWithoutExtension(inputFile) + ".puml"); + + using (var stream = new FileStream(inputFile, FileMode.Open, FileAccess.Read)) + { + var tree = CSharpSyntaxTree.ParseText(SourceText.From(stream)); + var root = tree.GetRoot(); + Accessibilities ignoreAcc = IPlantUmlGenerator.GetIgnoreAccessibilities(parameters); + + using var filestream = new FileStream(outputFile, FileMode.Create, FileAccess.Write); + using var writer = new StreamWriter(filestream); + var gen = new ClassDiagramGenerator( + writer, + " ", + ignoreAcc, + parameters.ContainsKey("-createAssociation"), + parameters.ContainsKey("-attributeRequired"), + excludeUmlBeginEndTags, + parameters.ContainsKey("-addPackageTags"), + parameters.ContainsKey("-removeSystemCollectionsAssociations"), + parameters.ContainsKey("-noGetSetForProperties"), + parameters.ContainsKey("-saveFields")); + gen.Generate(root); + relationships.AddAll(gen.relationships); + } + + if (parameters.ContainsKey("-allInOne")) + { + var lines = File.ReadAllLines(outputFile); + if (!excludeUmlBeginEndTags) + { + lines = lines.Skip(1).SkipLast(1).ToArray(); + } + foreach (string line in lines) + { + includeRefs.AppendLine(line); + } + } + else + { + var newRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @".\" : @"."; + includeRefs.AppendLine("!include " + outputFile.Replace(outputRoot, newRoot)); + } + } + catch (Exception e) + { + Console.WriteLine(e); + error = true; + } + } + + if (parameters.ContainsKey("-addPackageTags")) + { + var lines = ClassDiagramGenerator.GenerateRelationships(relationships); + foreach (string line in lines.Distinct()) + { + includeRefs.AppendLine(line); + } + } + + if (!excludeUmlBeginEndTags) includeRefs.AppendLine("@enduml"); + File.WriteAllText(PathHelper.CombinePath(outputRoot, "include.puml"), includeRefs.ToString()); + + if (error) + { + Console.WriteLine("There were files that could not be processed."); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromFileGenerator.cs b/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromFileGenerator.cs new file mode 100644 index 0000000..d7a8651 --- /dev/null +++ b/src/PlantUmlClassDiagramGenerator/Generator/PlantUmlFromFileGenerator.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using PlantUmlClassDiagramGenerator.Library; +using PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; + +namespace PlantUmlClassDiagramGenerator.Generator; + +public class PlantUmlFromFileGenerator : IPlantUmlGenerator +{ + public bool GeneratePlantUml(Dictionary parameters) + { + var inputFileName = parameters["in"]; + if (!File.Exists(inputFileName)) + { + Console.WriteLine($"\"{inputFileName}\" does not exist."); + return false; + } + string outputFileName; + if (parameters.TryGetValue("out", out string value)) + { + outputFileName = value; + try + { + var outdir = Path.GetDirectoryName(outputFileName); + Directory.CreateDirectory(outdir); + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + else + { + outputFileName = PathHelper.CombinePath(Path.GetDirectoryName(inputFileName), Path.GetFileNameWithoutExtension(inputFileName) + ".puml"); + } + + try + { + using var stream = new FileStream(inputFileName, FileMode.Open, FileAccess.Read); + var tree = CSharpSyntaxTree.ParseText(SourceText.From(stream)); + var root = tree.GetRoot(); + Accessibilities ignoreAcc = IPlantUmlGenerator.GetIgnoreAccessibilities(parameters); + + using var filestream = new FileStream(outputFileName, FileMode.Create, FileAccess.Write); + using var writer = new StreamWriter(filestream); + var gen = new ClassDiagramGenerator( + writer, + " ", + ignoreAcc, + parameters.ContainsKey("-createAssociation"), + parameters.ContainsKey("-attributeRequired"), + parameters.ContainsKey("-excludeUmlBeginEndTags"), + false, + parameters.ContainsKey("-removeSystemCollectionsAssociations"), + parameters.ContainsKey("-noGetSetForProperties")); + gen.Generate(root); + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/PlantUmlClassDiagramGenerator/Program.cs b/src/PlantUmlClassDiagramGenerator/Program.cs index 864acbf..85c4530 100644 --- a/src/PlantUmlClassDiagramGenerator/Program.cs +++ b/src/PlantUmlClassDiagramGenerator/Program.cs @@ -7,11 +7,15 @@ using System.Text; using PlantUmlClassDiagramGenerator.Library; using System.Runtime.InteropServices; +using PlantUmlClassDiagramGenerator.Generator; +using PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; namespace PlantUmlClassDiagramGenerator; class Program { + private static IPlantUmlGenerator generator; + enum OptionType { Value, @@ -27,7 +31,10 @@ enum OptionType ["-createAssociation"] = OptionType.Switch, ["-allInOne"] = OptionType.Switch, ["-attributeRequired"] = OptionType.Switch, - ["-excludeUmlBeginEndTags"] = OptionType.Switch + ["-excludeUmlBeginEndTags"] = OptionType.Switch, + ["-addPackageTags"] = OptionType.Switch, + ["-removeSystemCollectionsAssociations"] = OptionType.Switch, + ["-noGetSetForProperties"] = OptionType.Switch }; static int Main(string[] args) @@ -38,207 +45,16 @@ static int Main(string[] args) Console.WriteLine("Specify a source file name or directory name."); return -1; } - if (parameters.ContainsKey("-dir")) - { - if (!GeneratePlantUmlFromDir(parameters)) { return -1; } - } - else - { - if (!GeneratePlantUmlFromFile(parameters)) { return -1; } - } - return 0; - } - private static bool GeneratePlantUmlFromFile(Dictionary parameters) - { - var inputFileName = parameters["in"]; - if (!File.Exists(inputFileName)) - { - Console.WriteLine($"\"{inputFileName}\" does not exist."); - return false; - } - string outputFileName; - if (parameters.TryGetValue("out", out string value)) - { - outputFileName = value; - try - { - var outdir = Path.GetDirectoryName(outputFileName); - Directory.CreateDirectory(outdir); - } - catch (Exception e) - { - Console.WriteLine(e); - return false; - } - } + if (parameters.ContainsKey("-dir")) + generator = new PlantUmlFromDirGenerator(); else - { - outputFileName = CombinePath(Path.GetDirectoryName(inputFileName), - Path.GetFileNameWithoutExtension(inputFileName) + ".puml"); - } - - try - { - using var stream = new FileStream(inputFileName, FileMode.Open, FileAccess.Read); - var tree = CSharpSyntaxTree.ParseText(SourceText.From(stream)); - var root = tree.GetRoot(); - Accessibilities ignoreAcc = GetIgnoreAccessibilities(parameters); - - using var filestream = new FileStream(outputFileName, FileMode.Create, FileAccess.Write); - using var writer = new StreamWriter(filestream); - var gen = new ClassDiagramGenerator( - writer, - " ", - ignoreAcc, - parameters.ContainsKey("-createAssociation"), - parameters.ContainsKey("-attributeRequired"), - parameters.ContainsKey("-excludeUmlBeginEndTags")); - gen.Generate(root); - } - catch (Exception e) - { - Console.WriteLine(e); - return false; - } - return true; - } - - private static bool GeneratePlantUmlFromDir(Dictionary parameters) - { - var inputRoot = parameters["in"]; - if (!Directory.Exists(inputRoot)) - { - Console.WriteLine($"Directory \"{inputRoot}\" does not exist."); - return false; - } - - // Use GetFullPath to fully support relative paths. - var outputRoot = Path.GetFullPath(inputRoot); - if (parameters.TryGetValue("out", out string outValue)) - { - outputRoot = outValue; - try - { - Directory.CreateDirectory(outputRoot); - } - catch (Exception e) - { - Console.WriteLine(e); - return false; - } - } - - var excludePaths = new List(); - var pumlexclude = CombinePath(inputRoot, ".pumlexclude"); - if (File.Exists(pumlexclude)) - { - excludePaths = File - .ReadAllLines(pumlexclude) - .Where(path => !string.IsNullOrWhiteSpace(path)) - .Select(path => path.Trim()) - .ToList(); - } - if (parameters.TryGetValue("-excludePaths", out string excludePathValue)) - { - var splitOptions = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; - excludePaths.AddRange(excludePathValue.Split(',', splitOptions)); - } - - var excludeUmlBeginEndTags = parameters.ContainsKey("-excludeUmlBeginEndTags"); - var files = Directory.EnumerateFiles(inputRoot, "*.cs", SearchOption.AllDirectories); - - var includeRefs = new StringBuilder(); - if (!excludeUmlBeginEndTags) includeRefs.AppendLine("@startuml"); - - var error = false; - var filesToProcess = ExcludeFileFilter.GetFilesToProcess(files, excludePaths, inputRoot); - foreach (var inputFile in filesToProcess) - { - Console.WriteLine($"Processing \"{inputFile}\"..."); - try - { - var outputDir = CombinePath(outputRoot, Path.GetDirectoryName(inputFile).Replace(inputRoot, "")); - Directory.CreateDirectory(outputDir); - var outputFile = CombinePath(outputDir, - Path.GetFileNameWithoutExtension(inputFile) + ".puml"); - - using (var stream = new FileStream(inputFile, FileMode.Open, FileAccess.Read)) - { - var tree = CSharpSyntaxTree.ParseText(SourceText.From(stream)); - var root = tree.GetRoot(); - Accessibilities ignoreAcc = GetIgnoreAccessibilities(parameters); - - using var filestream = new FileStream(outputFile, FileMode.Create, FileAccess.Write); - using var writer = new StreamWriter(filestream); - var gen = new ClassDiagramGenerator( - writer, - " ", - ignoreAcc, - parameters.ContainsKey("-createAssociation"), - parameters.ContainsKey("-attributeRequired"), - excludeUmlBeginEndTags); - gen.Generate(root); - } - - if (parameters.ContainsKey("-allInOne")) - { - var lines = File.ReadAllLines(outputFile); - if (!excludeUmlBeginEndTags) - { - lines = lines.Skip(1).SkipLast(1).ToArray(); - } - foreach (string line in lines) - { - includeRefs.AppendLine(line); - } - } - else - { - var newRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @".\" : @"."; - includeRefs.AppendLine("!include " + outputFile.Replace(outputRoot, newRoot)); - } - } - catch (Exception e) - { - Console.WriteLine(e); - error = true; - } - } - if (!excludeUmlBeginEndTags) includeRefs.AppendLine("@enduml"); - File.WriteAllText(CombinePath(outputRoot, "include.puml"), includeRefs.ToString()); + generator = new PlantUmlFromFileGenerator(); - if (error) - { - Console.WriteLine("There were files that could not be processed."); - return false; - } - return true; + return !generator.GeneratePlantUml(parameters) ? 1 : 0; } - private static Accessibilities GetIgnoreAccessibilities(Dictionary parameters) - { - var ignoreAcc = Accessibilities.None; - if (parameters.ContainsKey("-public")) - { - ignoreAcc = Accessibilities.Private | Accessibilities.Internal - | Accessibilities.Protected | Accessibilities.ProtectedInternal; - } - else if (parameters.TryGetValue("-ignore", out string value)) - { - var ignoreItems = value.Split(','); - foreach (var item in ignoreItems) - { - if (Enum.TryParse(item, true, out Accessibilities acc)) - { - ignoreAcc |= acc; - } - } - } - return ignoreAcc; - } - private static Dictionary MakeParameters(string[] args) { var currentKey = ""; @@ -266,19 +82,13 @@ private static Dictionary MakeParameters(string[] args) } else { - if(!parameters.TryAdd("in", arg)) + if (!parameters.TryAdd("in", arg)) { parameters.TryAdd("out", arg); - } + } } - - } - return parameters; - } - private static string CombinePath(string first, string second) - { - return PathHelper.CombinePath(first, second); + return parameters; } } \ No newline at end of file diff --git a/test/PlantUmlClassDiagramGeneratorTest/ClassDiagramGeneratorTest.cs b/test/PlantUmlClassDiagramGeneratorTest/ClassDiagramGeneratorTest.cs index d37caa2..eaf022f 100644 --- a/test/PlantUmlClassDiagramGeneratorTest/ClassDiagramGeneratorTest.cs +++ b/test/PlantUmlClassDiagramGeneratorTest/ClassDiagramGeneratorTest.cs @@ -3,6 +3,7 @@ using System.Text; using System.IO; using PlantUmlClassDiagramGenerator.Library; +using PlantUmlClassDiagramGenerator.Library.ClassDiagramGenerator; using Xunit; using Xunit.Abstractions;