diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d9226a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,117 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +indent_style = space +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true:suggestion +# this. preferences +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_property = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_event = true:warning +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_prefer_inferred_tuple_names = true:suggestion +dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = false:warning +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true +# Expression-bodied members +csharp_style_expression_bodied_methods = false:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_operators = false:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = false diff --git a/GitVersion.yml b/GitVersion.yml index 4c4932b..09e2e36 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -mode: Mainline +mode: ContinuousDelivery branches: {} ignore: sha: [] diff --git a/LivingDocumentation.sln b/LivingDocumentation.sln index 7c54e10..147bad9 100644 --- a/LivingDocumentation.sln +++ b/LivingDocumentation.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 -VisualStudioVersion = 16.0.29009.5 +VisualStudioVersion = 16.0.29318.209 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivingDocumentation.Analyzer", "src\LivingDocumentation.Analyzer\LivingDocumentation.Analyzer.csproj", "{72E336A0-A9D8-494D-9332-CDE8341B247B}" EndProject @@ -27,7 +27,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{742D EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{93C52A66-6790-47D3-90A7-26EFFC41D4C6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LivingDocumentation.Analyzer.Tests", "tests\LivingDocumentation.Analyzer.Tests\LivingDocumentation.Analyzer.Tests.csproj", "{90E0C6A2-BF79-406D-BC5C-2637AD2BADE2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivingDocumentation.Analyzer.Tests", "tests\LivingDocumentation.Analyzer.Tests\LivingDocumentation.Analyzer.Tests.csproj", "{90E0C6A2-BF79-406D-BC5C-2637AD2BADE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4FC1E9E3-E30A-47E2-BBFD-D1105EF4710F}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + GitVersion.yml = GitVersion.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LivingDocumentation.RenderExtensions.Tests", "tests\LivingDocumentation.RenderExtensions.Tests\LivingDocumentation.RenderExtensions.Tests.csproj", "{0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -75,6 +83,10 @@ Global {90E0C6A2-BF79-406D-BC5C-2637AD2BADE2}.Debug|Any CPU.Build.0 = Debug|Any CPU {90E0C6A2-BF79-406D-BC5C-2637AD2BADE2}.Release|Any CPU.ActiveCfg = Release|Any CPU {90E0C6A2-BF79-406D-BC5C-2637AD2BADE2}.Release|Any CPU.Build.0 = Release|Any CPU + {0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,6 +102,7 @@ Global {43F54404-225C-4234-AFA9-A316A891884C} = {2CCF1B57-05F0-4DF7-8B48-29D03CE052F5} {4CC5B483-9205-47A4-82F8-189047316512} = {2CCF1B57-05F0-4DF7-8B48-29D03CE052F5} {90E0C6A2-BF79-406D-BC5C-2637AD2BADE2} = {93C52A66-6790-47D3-90A7-26EFFC41D4C6} + {0A28D3CE-00A8-4473-B571-F8AE0FFFF1AC} = {93C52A66-6790-47D3-90A7-26EFFC41D4C6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {298BF1F6-9407-40A1-91C0-F11BA01BD95B} diff --git a/samples/LivingDocumentation.eShopOnContainers/AsciiDocRenderer.cs b/samples/LivingDocumentation.eShopOnContainers/AsciiDocRenderer.cs index 5ebace4..25f4e8b 100644 --- a/samples/LivingDocumentation.eShopOnContainers/AsciiDocRenderer.cs +++ b/samples/LivingDocumentation.eShopOnContainers/AsciiDocRenderer.cs @@ -1,265 +1,265 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; - -namespace LivingDocumentation.eShopOnContainers -{ - public class AsciiDocRenderer - { - private readonly IReadOnlyList types; - private readonly IReadOnlyDictionary aggregateFiles; - private readonly IReadOnlyDictionary commandHandlerFiles; - private readonly IReadOnlyDictionary eventHandlerFiles; - private static readonly Regex replaceTypeSuffix = new Regex("(?:(?:Command|(?:Domain|Integration)Event))(?:Handler)?$", RegexOptions.CultureInvariant); - - public AsciiDocRenderer(IReadOnlyList types, IReadOnlyDictionary aggregateFiles, IReadOnlyDictionary commandHandlerFiles, IReadOnlyDictionary eventHandlerFiles) - { - this.types = types; - this.aggregateFiles = aggregateFiles; - this.commandHandlerFiles = commandHandlerFiles; - this.eventHandlerFiles = eventHandlerFiles; - } - - public void Render() - { - var stringBuilder = new StringBuilder(); - - RenderFileHeader(stringBuilder); - - RenderAggregates(stringBuilder); - RenderCommands(stringBuilder); - RenderCommandHandlers(stringBuilder); - RenderDomainEvents(stringBuilder); - RenderDomainEventHandlers(stringBuilder); - RenderIntegrationEvents(stringBuilder); - - File.WriteAllText("documentation.generated.adoc", stringBuilder.ToString()); - } - - private void RenderAggregates(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::aggregates[]"); - stringBuilder.AppendLine("== Aggregates"); - stringBuilder.AppendLine("Aggregates in the eShop application."); - - foreach (var (type, path) in this.aggregateFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"// tag::aggregate-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - stringBuilder.AppendLine($"=== {type.Name}"); - stringBuilder.AppendLine($"The \"`{type.Name.ToLower()}`\" aggregate."); - stringBuilder.AppendLine(); - stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"[plantuml, aggregate.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"include::{path}[]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"// end::aggregate-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - } - - stringBuilder.AppendLine("// end::aggregates[]"); - } - - private void RenderCommands(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::commands[]"); - stringBuilder.AppendLine("== Commands"); - stringBuilder.AppendLine("Commands in the eShop application."); - - foreach (var type in this.types.Where(t => t.IsCommand() && !t.FullName.IsGeneric()).OrderBy(t => t.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" command."); - stringBuilder.AppendLine(); - - if (!string.IsNullOrWhiteSpace(type.Documentation)) - { - stringBuilder.AppendLine(type.Documentation); - stringBuilder.AppendLine(); - } - - stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); - stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); - stringBuilder.AppendLine("|==="); - stringBuilder.AppendLine("|Name|Type"); - - foreach (var property in type.Properties) - { - stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); - } - - stringBuilder.AppendLine("|==="); - } - - stringBuilder.AppendLine("// end::commands[]"); - } - - private void RenderCommandHandlers(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::commandhandlers[]"); - stringBuilder.AppendLine("== Command Handlers"); - stringBuilder.AppendLine("Command handlers in the eShop application."); - - foreach (var (type, path) in this.commandHandlerFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"// tag::commandhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" command handler."); - - if (!string.IsNullOrWhiteSpace(type.Documentation)) - { - stringBuilder.AppendLine(type.Documentation); - stringBuilder.AppendLine(); - } - - stringBuilder.AppendLine(); - stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"[plantuml, commandhandler.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"include::{path}[]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"// end::commandhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - } - - stringBuilder.AppendLine("// end::commandhandlers[]"); - } - - private void RenderDomainEvents(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::domainevents[]"); - stringBuilder.AppendLine("== Domain Events"); - stringBuilder.AppendLine("Domain events in the eShop application."); - - foreach (var type in this.types.Where(t => t.IsDomainEvent()).OrderBy(t => t.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" domain event."); - stringBuilder.AppendLine(); - - if (!string.IsNullOrWhiteSpace(type.Documentation)) - { - stringBuilder.AppendLine(type.Documentation); - stringBuilder.AppendLine(); - } - - stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); - stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); - stringBuilder.AppendLine("|==="); - stringBuilder.AppendLine("|Name|Type"); - - foreach (var property in type.Properties) - { - stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); - } - - stringBuilder.AppendLine("|==="); - } - - stringBuilder.AppendLine("// end::domainevents[]"); - } - - private void RenderDomainEventHandlers(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::domaineventhandlers[]"); - stringBuilder.AppendLine("== Domain Event Handlers"); - stringBuilder.AppendLine("Domain event handlers in the eShop application."); - - foreach (var (type, path) in this.eventHandlerFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"// tag::domaineventhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" event handler."); - - if (!string.IsNullOrWhiteSpace(type.Documentation)) - { - stringBuilder.AppendLine(type.Documentation); - stringBuilder.AppendLine(); - } - - stringBuilder.AppendLine(); - stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"[plantuml, domaineventhandler.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"include::{path}[]"); - stringBuilder.AppendLine("...."); - stringBuilder.AppendLine($"// end::domaineventhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); - } - - stringBuilder.AppendLine("// end::domaineventhandlers[]"); - } - private void RenderIntegrationEvents(StringBuilder stringBuilder) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine("// tag::integrationevents[]"); - stringBuilder.AppendLine("== Integration Events"); - stringBuilder.AppendLine("Integration events in the eShop application."); - - foreach (var type in this.types.Where(t => t.IsIntegrationEvent() && t.FullName.StartsWith("Ordering.API", StringComparison.Ordinal)).OrderBy(t => t.Name)) - { - stringBuilder.AppendLine(); - stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); - stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" integration event."); - stringBuilder.AppendLine(); - - if (!string.IsNullOrWhiteSpace(type.Documentation)) - { - stringBuilder.AppendLine(type.Documentation); - stringBuilder.AppendLine(); - } - - stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); - stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); - stringBuilder.AppendLine("|==="); - stringBuilder.AppendLine("|Name|Type"); - - foreach (var property in type.Properties) - { - stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); - } - - stringBuilder.AppendLine("|==="); - } - - stringBuilder.AppendLine("// end::integrationevents[]"); - } - - private static void RenderFileHeader(StringBuilder stringBuilder) - { - stringBuilder.AppendLine("= Generated documentation"); - stringBuilder.AppendLine("Michaël Hompus "); - stringBuilder.AppendLine($"{Assembly.GetEntryAssembly().GetName().Version.ToString(3)}, {DateTime.Today:yyyy-MM-dd}"); - stringBuilder.AppendLine(":toc: left"); - stringBuilder.AppendLine(":toc-level: 2"); - stringBuilder.AppendLine(":sectnums:"); - stringBuilder.AppendLine(":icons: font"); - stringBuilder.AppendLine(); - stringBuilder.AppendLine("NOTE: This document has been automatically generated"); - stringBuilder.AppendLine(); - stringBuilder.AppendLine("The documentation is generated from the source code of https://github.com/dotnet-architecture/eShopOnContainers[eShopOnContainers^]."); - stringBuilder.AppendLine(); - } - - private static string StripTypeSuffix(string name) - { - return replaceTypeSuffix.Replace(name, string.Empty); - } - - private static string FormatTechnicalName(string name) - { - return StripTypeSuffix(name).ToSentenceCase(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace LivingDocumentation.eShopOnContainers +{ + public class AsciiDocRenderer + { + private readonly IReadOnlyList types; + private readonly IReadOnlyDictionary aggregateFiles; + private readonly IReadOnlyDictionary commandHandlerFiles; + private readonly IReadOnlyDictionary eventHandlerFiles; + private static readonly Regex replaceTypeSuffix = new Regex("(?:(?:Command|(?:Domain|Integration)Event))(?:Handler)?$", RegexOptions.CultureInvariant); + + public AsciiDocRenderer(IReadOnlyList types, IReadOnlyDictionary aggregateFiles, IReadOnlyDictionary commandHandlerFiles, IReadOnlyDictionary eventHandlerFiles) + { + this.types = types; + this.aggregateFiles = aggregateFiles; + this.commandHandlerFiles = commandHandlerFiles; + this.eventHandlerFiles = eventHandlerFiles; + } + + public void Render() + { + var stringBuilder = new StringBuilder(); + + RenderFileHeader(stringBuilder); + + this.RenderAggregates(stringBuilder); + this.RenderCommands(stringBuilder); + this.RenderCommandHandlers(stringBuilder); + this.RenderDomainEvents(stringBuilder); + this.RenderDomainEventHandlers(stringBuilder); + this.RenderIntegrationEvents(stringBuilder); + + File.WriteAllText("documentation.generated.adoc", stringBuilder.ToString()); + } + + private void RenderAggregates(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::aggregates[]"); + stringBuilder.AppendLine("== Aggregates"); + stringBuilder.AppendLine("Aggregates in the eShop application."); + + foreach (var (type, path) in this.aggregateFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"// tag::aggregate-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + stringBuilder.AppendLine($"=== {type.Name}"); + stringBuilder.AppendLine($"The \"`{type.Name.ToLower()}`\" aggregate."); + stringBuilder.AppendLine(); + stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"[plantuml, aggregate.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"include::{path}[]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"// end::aggregate-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + } + + stringBuilder.AppendLine("// end::aggregates[]"); + } + + private void RenderCommands(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::commands[]"); + stringBuilder.AppendLine("== Commands"); + stringBuilder.AppendLine("Commands in the eShop application."); + + foreach (var type in this.types.Where(t => t.IsCommand() && !t.FullName.IsGeneric()).OrderBy(t => t.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" command."); + stringBuilder.AppendLine(); + + if (!string.IsNullOrWhiteSpace(type.DocumentationComments?.Summary)) + { + stringBuilder.AppendLine(type.DocumentationComments.Summary); + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); + stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); + stringBuilder.AppendLine("|==="); + stringBuilder.AppendLine("|Name|Type"); + + foreach (var property in type.Properties) + { + stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); + } + + stringBuilder.AppendLine("|==="); + } + + stringBuilder.AppendLine("// end::commands[]"); + } + + private void RenderCommandHandlers(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::commandhandlers[]"); + stringBuilder.AppendLine("== Command Handlers"); + stringBuilder.AppendLine("Command handlers in the eShop application."); + + foreach (var (type, path) in this.commandHandlerFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"// tag::commandhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" command handler."); + + if (!string.IsNullOrWhiteSpace(type.DocumentationComments?.Summary)) + { + stringBuilder.AppendLine(type.DocumentationComments.Summary); + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine(); + stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"[plantuml, commandhandler.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"include::{path}[]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"// end::commandhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + } + + stringBuilder.AppendLine("// end::commandhandlers[]"); + } + + private void RenderDomainEvents(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::domainevents[]"); + stringBuilder.AppendLine("== Domain Events"); + stringBuilder.AppendLine("Domain events in the eShop application."); + + foreach (var type in this.types.Where(t => t.IsDomainEvent()).OrderBy(t => t.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" domain event."); + stringBuilder.AppendLine(); + + if (!string.IsNullOrWhiteSpace(type.DocumentationComments?.Summary)) + { + stringBuilder.AppendLine(type.DocumentationComments.Summary); + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); + stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); + stringBuilder.AppendLine("|==="); + stringBuilder.AppendLine("|Name|Type"); + + foreach (var property in type.Properties) + { + stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); + } + + stringBuilder.AppendLine("|==="); + } + + stringBuilder.AppendLine("// end::domainevents[]"); + } + + private void RenderDomainEventHandlers(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::domaineventhandlers[]"); + stringBuilder.AppendLine("== Domain Event Handlers"); + stringBuilder.AppendLine("Domain event handlers in the eShop application."); + + foreach (var (type, path) in this.eventHandlerFiles.Select(kv => (Type: this.types.FirstOrDefault(kv.Key), Path: kv.Value)).OrderBy(t => t.Type.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"// tag::domaineventhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" event handler."); + + if (!string.IsNullOrWhiteSpace(type.DocumentationComments?.Summary)) + { + stringBuilder.AppendLine(type.DocumentationComments.Summary); + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine(); + stringBuilder.AppendLine($".{FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"[plantuml, domaineventhandler.{StripTypeSuffix(type.Name).ToLowerInvariant()}, png]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"include::{path}[]"); + stringBuilder.AppendLine("...."); + stringBuilder.AppendLine($"// end::domaineventhandler-{StripTypeSuffix(type.Name).ToLowerInvariant()}[]"); + } + + stringBuilder.AppendLine("// end::domaineventhandlers[]"); + } + private void RenderIntegrationEvents(StringBuilder stringBuilder) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine("// tag::integrationevents[]"); + stringBuilder.AppendLine("== Integration Events"); + stringBuilder.AppendLine("Integration events in the eShop application."); + + foreach (var type in this.types.Where(t => t.IsIntegrationEvent() && t.FullName.StartsWith("Ordering.API", StringComparison.Ordinal)).OrderBy(t => t.Name)) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine($"=== {FormatTechnicalName(type.Name)}"); + stringBuilder.AppendLine($"The \"`{FormatTechnicalName(type.Name).ToLower()}`\" integration event."); + stringBuilder.AppendLine(); + + if (!string.IsNullOrWhiteSpace(type.DocumentationComments?.Summary)) + { + stringBuilder.AppendLine(type.DocumentationComments.Summary); + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine($".{type.Name.ToSentenceCase()} Fields"); + stringBuilder.AppendLine("[%header%,width=\"75%\",cols=\"2h,3d\"]"); + stringBuilder.AppendLine("|==="); + stringBuilder.AppendLine("|Name|Type"); + + foreach (var property in type.Properties) + { + stringBuilder.AppendLine($"|{property.Name}|{property.Type.ForDiagram()}"); + } + + stringBuilder.AppendLine("|==="); + } + + stringBuilder.AppendLine("// end::integrationevents[]"); + } + + private static void RenderFileHeader(StringBuilder stringBuilder) + { + stringBuilder.AppendLine("= Generated documentation"); + stringBuilder.AppendLine("Michaël Hompus "); + stringBuilder.AppendLine($"{Assembly.GetEntryAssembly().GetName().Version.ToString(3)}, {DateTime.Today:yyyy-MM-dd}"); + stringBuilder.AppendLine(":toc: left"); + stringBuilder.AppendLine(":toc-level: 2"); + stringBuilder.AppendLine(":sectnums:"); + stringBuilder.AppendLine(":icons: font"); + stringBuilder.AppendLine(); + stringBuilder.AppendLine("NOTE: This document has been automatically generated"); + stringBuilder.AppendLine(); + stringBuilder.AppendLine("The documentation is generated from the source code of https://github.com/dotnet-architecture/eShopOnContainers[eShopOnContainers^]."); + stringBuilder.AppendLine(); + } + + private static string StripTypeSuffix(string name) + { + return replaceTypeSuffix.Replace(name, string.Empty); + } + + private static string FormatTechnicalName(string name) + { + return StripTypeSuffix(name).ToSentenceCase(); + } + } +} diff --git a/samples/LivingDocumentation.eShopOnContainers/Program.cs b/samples/LivingDocumentation.eShopOnContainers/Program.cs index 413cf91..abe9afe 100644 --- a/samples/LivingDocumentation.eShopOnContainers/Program.cs +++ b/samples/LivingDocumentation.eShopOnContainers/Program.cs @@ -14,8 +14,7 @@ public static async Task Main(string[] args) var serializerSettings = new JsonSerializerSettings { - DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, - Formatting = Formatting.None, + DefaultValueHandling = DefaultValueHandling.Include, ContractResolver = new SkipEmptyCollectionsContractResolver(), TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameHandling = TypeNameHandling.Auto diff --git a/samples/LivingDocumentation.eShopOnContainers/eShopOnContainerExtensions.cs b/samples/LivingDocumentation.eShopOnContainers/eShopOnContainerExtensions.cs index 7f35bdf..fb4a39d 100644 --- a/samples/LivingDocumentation.eShopOnContainers/eShopOnContainerExtensions.cs +++ b/samples/LivingDocumentation.eShopOnContainers/eShopOnContainerExtensions.cs @@ -1,4 +1,4 @@ -using LivingDocumentation.Uml; +using LivingDocumentation.Uml; using System; using System.Linq; using System.Text; @@ -7,11 +7,11 @@ namespace LivingDocumentation.eShopOnContainers { public static class eShopOnContainersExtensions { - const string IntegrationEvent = "IntegrationEvent"; - const string DomainEvent = "DomainEvent"; - const string DomainEventHandler = "DomainEventHandler"; - const string Command = "Command"; - const string CommandHandler = "CommandHandler"; + private const string IntegrationEvent = "IntegrationEvent"; + private const string DomainEvent = "DomainEvent"; + private const string DomainEventHandler = "DomainEventHandler"; + private const string Command = "Command"; + private const string CommandHandler = "CommandHandler"; public static string ArrowColor(this string name) { diff --git a/src/LivingDocumentation.Abstractions/IHaveDocumentationComments.cs b/src/LivingDocumentation.Abstractions/IHaveDocumentationComments.cs new file mode 100644 index 0000000..66b0d19 --- /dev/null +++ b/src/LivingDocumentation.Abstractions/IHaveDocumentationComments.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace LivingDocumentation +{ + public interface IHaveDocumentationComments + { + string Example { get; } + + IDictionary Exceptions { get; } + + IDictionary Params { get; } + + IDictionary Permissions { get; } + + string Remarks { get; } + + string Returns { get; } + + IDictionary SeeAlsos { get; } + + string Summary { get; } + + IDictionary TypeParams { get; } + + string Value { get; } + } +} diff --git a/src/LivingDocumentation.Abstractions/IHaveMethodBody.cs b/src/LivingDocumentation.Abstractions/IHaveMethodBody.cs index 8f5f485..2f3d1f8 100644 --- a/src/LivingDocumentation.Abstractions/IHaveMethodBody.cs +++ b/src/LivingDocumentation.Abstractions/IHaveMethodBody.cs @@ -4,6 +4,8 @@ namespace LivingDocumentation { public interface IHaveAMethodBody : IHaveModifiers { + IHaveDocumentationComments DocumentationComments { get; set; } + string Name { get; } List Parameters { get; } diff --git a/src/LivingDocumentation.Abstractions/IMemberable.cs b/src/LivingDocumentation.Abstractions/IMemberable.cs index cbfdac8..51cceb9 100644 --- a/src/LivingDocumentation.Abstractions/IMemberable.cs +++ b/src/LivingDocumentation.Abstractions/IMemberable.cs @@ -10,6 +10,6 @@ public interface IMemberable : IHaveModifiers [JsonProperty(Order = -3)] string Name { get; } - string Documentation { get; } + IHaveDocumentationComments DocumentationComments { get; } } -} \ No newline at end of file +} diff --git a/src/LivingDocumentation.Abstractions/MemberType.cs b/src/LivingDocumentation.Abstractions/MemberType.cs index f90a9b2..fd8e7a1 100644 --- a/src/LivingDocumentation.Abstractions/MemberType.cs +++ b/src/LivingDocumentation.Abstractions/MemberType.cs @@ -6,6 +6,7 @@ public enum MemberType Method = 1, Property = 2, Constructor = 3, - EnumMember = 4 + EnumMember = 4, + Event = 5 } -} \ No newline at end of file +} diff --git a/src/LivingDocumentation.Analyzer/Analyzers/BranchingAnalyzer.cs b/src/LivingDocumentation.Analyzer/Analyzers/BranchingAnalyzer.cs index da56601..6c99887 100644 --- a/src/LivingDocumentation.Analyzer/Analyzers/BranchingAnalyzer.cs +++ b/src/LivingDocumentation.Analyzer/Analyzers/BranchingAnalyzer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -21,14 +20,14 @@ public BranchingAnalyzer(in SemanticModel semanticModel, IList statem public override void VisitIfStatement(IfStatementSyntax node) { var ifStatement = new If(); - statements.Add(ifStatement); + this.statements.Add(ifStatement); var ifSection = new IfElseSection(); ifStatement.Sections.Add(ifSection); ifSection.Condition = node.Condition.ToString(); - var ifInvocationAnalyzer = new InvocationsAnalyzer(semanticModel, ifSection.Statements); + var ifInvocationAnalyzer = new InvocationsAnalyzer(this.semanticModel, ifSection.Statements); ifInvocationAnalyzer.Visit(node.Statement); var elseNode = node.Else; @@ -37,7 +36,7 @@ public override void VisitIfStatement(IfStatementSyntax node) var section = new IfElseSection(); ifStatement.Sections.Add(section); - var elseInvocationAnalyzer = new InvocationsAnalyzer(semanticModel, section.Statements); + var elseInvocationAnalyzer = new InvocationsAnalyzer(this.semanticModel, section.Statements); elseInvocationAnalyzer.Visit(elseNode.Statement); if (elseNode.Statement.IsKind(SyntaxKind.IfStatement)) @@ -57,7 +56,7 @@ public override void VisitIfStatement(IfStatementSyntax node) public override void VisitSwitchStatement(SwitchStatementSyntax node) { var switchStatement = new Switch(); - statements.Add(switchStatement); + this.statements.Add(switchStatement); switchStatement.Expression = node.Expression.ToString(); @@ -68,7 +67,7 @@ public override void VisitSwitchStatement(SwitchStatementSyntax node) switchSection.Labels.AddRange(section.Labels.Select(l => Label(l))); - var invocationAnalyzer = new InvocationsAnalyzer(semanticModel, switchSection.Statements); + var invocationAnalyzer = new InvocationsAnalyzer(this.semanticModel, switchSection.Statements); invocationAnalyzer.Visit(section); } } @@ -91,4 +90,4 @@ private static string Label(SwitchLabelSyntax label) } } } -} \ No newline at end of file +} diff --git a/src/LivingDocumentation.Analyzer/Analyzers/InvocationsAnalyzer.cs b/src/LivingDocumentation.Analyzer/Analyzers/InvocationsAnalyzer.cs index 01b1c50..d15378c 100644 --- a/src/LivingDocumentation.Analyzer/Analyzers/InvocationsAnalyzer.cs +++ b/src/LivingDocumentation.Analyzer/Analyzers/InvocationsAnalyzer.cs @@ -20,16 +20,16 @@ public InvocationsAnalyzer(in SemanticModel semanticModel, IList stat public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) { - string containingType = semanticModel.GetTypeDisplayString(node); + string containingType = this.semanticModel.GetTypeDisplayString(node); var invocation = new InvocationDescription(containingType, node.Type.ToString()); - statements.Add(invocation); + this.statements.Add(invocation); if (node.ArgumentList != null) { foreach (var argument in node.ArgumentList.Arguments) { - var argumentDescription = new ArgumentDescription(semanticModel.GetTypeDisplayString(argument.Expression), argument.Expression.ToString()); + var argumentDescription = new ArgumentDescription(this.semanticModel.GetTypeDisplayString(argument.Expression), argument.Expression.ToString()); invocation.Arguments.Add(argumentDescription); } } @@ -38,7 +38,7 @@ public override void VisitObjectCreationExpression(ObjectCreationExpressionSynta { foreach (var expression in node.Initializer.Expressions) { - var argumentDescription = new ArgumentDescription(semanticModel.GetTypeDisplayString(expression), expression.ToString()); + var argumentDescription = new ArgumentDescription(this.semanticModel.GetTypeDisplayString(expression), expression.ToString()); invocation.Arguments.Add(argumentDescription); } } @@ -48,41 +48,41 @@ public override void VisitObjectCreationExpression(ObjectCreationExpressionSynta public override void VisitSwitchStatement(SwitchStatementSyntax node) { - var branchingAnalyzer = new BranchingAnalyzer(semanticModel, statements); + var branchingAnalyzer = new BranchingAnalyzer(this.semanticModel, this.statements); branchingAnalyzer.Visit(node); } public override void VisitIfStatement(IfStatementSyntax node) { - var branchingAnalyzer = new BranchingAnalyzer(semanticModel, statements); + var branchingAnalyzer = new BranchingAnalyzer(this.semanticModel, this.statements); branchingAnalyzer.Visit(node); } public override void VisitForEachStatement(ForEachStatementSyntax node) { - var loopingAnalyzer = new LoopingAnalyzer(semanticModel, statements); + var loopingAnalyzer = new LoopingAnalyzer(this.semanticModel, this.statements); loopingAnalyzer.Visit(node); } public override void VisitInvocationExpression(InvocationExpressionSyntax node) { - if (Program.RuntimeOptions.VerboseOutput && semanticModel.GetTypeInfo(node).Type.Kind == SymbolKind.ErrorType) + if (Program.RuntimeOptions.VerboseOutput && this.semanticModel.GetTypeInfo(node).Type.Kind == SymbolKind.ErrorType) { Console.WriteLine("WARN: Could not resolve type of invocation of the following block:"); Console.WriteLine(node.ToFullString()); return; } - if (semanticModel.GetConstantValue(node).HasValue && string.Equals((node.Expression as IdentifierNameSyntax)?.Identifier.ValueText, "nameof")) + if (this.semanticModel.GetConstantValue(node).HasValue && string.Equals((node.Expression as IdentifierNameSyntax)?.Identifier.ValueText, "nameof", StringComparison.Ordinal)) { // nameof is compiler sugar, and is actually a method we are not interrested in return; } - string containingType = semanticModel.GetSymbolInfo(node.Expression).Symbol?.ContainingSymbol.ToDisplayString(); + string containingType = this.semanticModel.GetSymbolInfo(node.Expression).Symbol?.ContainingSymbol.ToDisplayString(); if (containingType == null) { - containingType = semanticModel.GetSymbolInfo(node.Expression).CandidateSymbols.FirstOrDefault()?.ContainingSymbol.ToDisplayString(); + containingType = this.semanticModel.GetSymbolInfo(node.Expression).CandidateSymbols.FirstOrDefault()?.ContainingSymbol.ToDisplayString(); } string methodName = string.Empty; @@ -96,17 +96,20 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) methodName = i.Identifier.ValueText; break; } + if (methodName == "Fire") + { - var invocation = new InvocationDescription(containingType, methodName); - statements.Add(invocation); + } + var invocation = new InvocationDescription(containingType, methodName); + this.statements.Add(invocation); foreach (var argument in node.ArgumentList.Arguments) { - var argumentDescription = new ArgumentDescription(semanticModel.GetTypeDisplayString(argument.Expression), argument.Expression.ToString()); + var argumentDescription = new ArgumentDescription(this.semanticModel.GetTypeDisplayString(argument.Expression), argument.Expression.ToString()); invocation.Arguments.Add(argumentDescription); } base.VisitInvocationExpression(node); } } -} \ No newline at end of file +} diff --git a/src/LivingDocumentation.Analyzer/Analyzers/LoopingAnalyzer.cs b/src/LivingDocumentation.Analyzer/Analyzers/LoopingAnalyzer.cs index 736722f..a6267fb 100644 --- a/src/LivingDocumentation.Analyzer/Analyzers/LoopingAnalyzer.cs +++ b/src/LivingDocumentation.Analyzer/Analyzers/LoopingAnalyzer.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -21,12 +19,12 @@ public LoopingAnalyzer(in SemanticModel semanticModel, IList statemen public override void VisitForEachStatement(ForEachStatementSyntax node) { var forEachStatement = new ForEach(); - statements.Add(forEachStatement); + this.statements.Add(forEachStatement); forEachStatement.Expression = $"{node.Identifier.ToString()} in {node.Expression.ToString()}"; - var invocationAnalyzer = new InvocationsAnalyzer(semanticModel, forEachStatement.Statements); + var invocationAnalyzer = new InvocationsAnalyzer(this.semanticModel, forEachStatement.Statements); invocationAnalyzer.Visit(node.Statement); } } -} \ No newline at end of file +} diff --git a/src/LivingDocumentation.Analyzer/Analyzers/SourceAnalyzer.cs b/src/LivingDocumentation.Analyzer/Analyzers/SourceAnalyzer.cs index f1b8523..d8de299 100644 --- a/src/LivingDocumentation.Analyzer/Analyzers/SourceAnalyzer.cs +++ b/src/LivingDocumentation.Analyzer/Analyzers/SourceAnalyzer.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -12,77 +11,95 @@ public class SourceAnalyzer : CSharpSyntaxWalker { private readonly SemanticModel semanticModel; private readonly IList types; - private readonly IReadOnlyList referencedAssemblies; private TypeDescription currentType = null; - public SourceAnalyzer(in SemanticModel semanticModel, IList types, IReadOnlyList referencedAssemblies) + public SourceAnalyzer(in SemanticModel semanticModel, IList types) { this.types = types; this.semanticModel = semanticModel; - this.referencedAssemblies = referencedAssemblies; } public override void VisitClassDeclaration(ClassDeclarationSyntax node) { - if (ProcessedEmbeddedType(node)) return; + if (this.ProcessedEmbeddedType(node)) return; - ExtractBaseTypeDeclaration(TypeType.Class, node); + this.ExtractBaseTypeDeclaration(TypeType.Class, node); base.VisitClassDeclaration(node); } public override void VisitEnumDeclaration(EnumDeclarationSyntax node) { - if (ProcessedEmbeddedType(node)) return; + if (this.ProcessedEmbeddedType(node)) return; - ExtractBaseTypeDeclaration(TypeType.Enum, node); + this.ExtractBaseTypeDeclaration(TypeType.Enum, node); base.VisitEnumDeclaration(node); } public override void VisitStructDeclaration(StructDeclarationSyntax node) { - if (ProcessedEmbeddedType(node)) return; + if (this.ProcessedEmbeddedType(node)) return; - ExtractBaseTypeDeclaration(TypeType.Struct, node); + this.ExtractBaseTypeDeclaration(TypeType.Struct, node); base.VisitStructDeclaration(node); } public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { - if (ProcessedEmbeddedType(node)) return; + if (this.ProcessedEmbeddedType(node)) return; - ExtractBaseTypeDeclaration(TypeType.Interface, node); + this.ExtractBaseTypeDeclaration(TypeType.Interface, node); base.VisitInterfaceDeclaration(node); } public override void VisitFieldDeclaration(FieldDeclarationSyntax node) { - var fieldDescription = new FieldDescription(semanticModel.GetTypeDisplayString(node.Declaration.Type), node.Declaration.Variables.First().Identifier.ValueText); - this.currentType.AddMember(fieldDescription); + foreach (var variable in node.Declaration.Variables) + { + var fieldDescription = new FieldDescription(this.semanticModel.GetTypeDisplayString(node.Declaration.Type), variable.Identifier.ValueText); + this.currentType.AddMember(fieldDescription); - fieldDescription.Modifiers |= ParseModifiers(node.Modifiers); - this.EnsureMemberDefaultAccessModifier(fieldDescription); + fieldDescription.Modifiers |= ParseModifiers(node.Modifiers); + this.EnsureMemberDefaultAccessModifier(fieldDescription); - fieldDescription.Initializer = node.Declaration.Variables.First().Initializer?.Value.ToString(); // Assumption: Field has only a single initializer - fieldDescription.Documentation = ExtractDocumentation(node); + fieldDescription.Initializer = variable.Initializer?.Value.ToString(); + fieldDescription.DocumentationComments = this.ExtractDocumentation(variable); + } base.VisitFieldDeclaration(node); } + public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) + { + foreach (var variable in node.Declaration.Variables) + { + var fieldDescription = new EventDescription(this.semanticModel.GetTypeDisplayString(node.Declaration.Type), variable.Identifier.ValueText); + this.currentType.AddMember(fieldDescription); + + fieldDescription.Modifiers |= ParseModifiers(node.Modifiers); + this.EnsureMemberDefaultAccessModifier(fieldDescription); + + fieldDescription.Initializer = variable.Initializer?.Value.ToString(); + fieldDescription.DocumentationComments = this.ExtractDocumentation(variable); + } + + base.VisitEventFieldDeclaration(node); + } + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) { - var propertyDescription = new PropertyDescription(semanticModel.GetTypeDisplayString(node.Type), node.Identifier.ToString()); + var propertyDescription = new PropertyDescription(this.semanticModel.GetTypeDisplayString(node.Type), node.Identifier.ToString()); this.currentType.AddMember(propertyDescription); propertyDescription.Modifiers |= ParseModifiers(node.Modifiers); this.EnsureMemberDefaultAccessModifier(propertyDescription); propertyDescription.Initializer = node.Initializer?.Value.ToString(); - propertyDescription.Documentation = ExtractDocumentation(node); + propertyDescription.DocumentationComments = this.ExtractDocumentation(node); base.VisitPropertyDeclaration(node); } @@ -93,7 +110,7 @@ public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node this.currentType.AddMember(enumMemberDescription); enumMemberDescription.Modifiers |= Modifier.Public; - enumMemberDescription.Documentation = ExtractDocumentation(node); + enumMemberDescription.DocumentationComments = this.ExtractDocumentation(node); base.VisitEnumMemberDeclaration(node); } @@ -103,24 +120,24 @@ public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax no var constructorDescription = new ConstructorDescription(node.Identifier.ToString()); this.currentType.AddMember(constructorDescription); - ExtractBaseMethodDeclaration(node, constructorDescription); + this.ExtractBaseMethodDeclaration(node, constructorDescription); base.VisitConstructorDeclaration(node); } public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { - var methodDescription = new MethodDescription(semanticModel.GetTypeInfo(node.ReturnType).Type.ToDisplayString(), node.Identifier.ToString()); + var methodDescription = new MethodDescription(this.semanticModel.GetTypeInfo(node.ReturnType).Type.ToDisplayString(), node.Identifier.ToString()); this.currentType.AddMember(methodDescription); - ExtractBaseMethodDeclaration(node, methodDescription); + this.ExtractBaseMethodDeclaration(node, methodDescription); base.VisitMethodDeclaration(node); } private void ExtractBaseTypeDeclaration(TypeType type, BaseTypeDeclarationSyntax node) { - this.currentType = new TypeDescription(type, semanticModel.GetDeclaredSymbol(node).ToDisplayString()); + this.currentType = new TypeDescription(type, this.semanticModel.GetDeclaredSymbol(node).ToDisplayString()); if (!this.types.Contains(this.currentType)) { this.types.Add(this.currentType); @@ -128,17 +145,17 @@ private void ExtractBaseTypeDeclaration(TypeType type, BaseTypeDeclarationSyntax if (node.BaseList != null) { - this.currentType.BaseTypes.AddRange(node.BaseList.Types.Select(t => semanticModel.GetTypeDisplayString(t.Type))); + this.currentType.BaseTypes.AddRange(node.BaseList.Types.Select(t => this.semanticModel.GetTypeDisplayString(t.Type))); } this.currentType.Modifiers |= ParseModifiers(node.Modifiers); this.EnsureTypeDefaultAccessModifier(node); - this.currentType.Documentation = ExtractDocumentation(node); + this.currentType.DocumentationComments = this.ExtractDocumentation(node); if (node.AttributeLists != null) { - ExtractAttributes(node); + this.ExtractAttributes(node); } } @@ -178,7 +195,7 @@ private bool ProcessedEmbeddedType(SyntaxNode node) return false; } - var embeddedAnalyzer = new SourceAnalyzer(semanticModel, types, referencedAssemblies); + var embeddedAnalyzer = new SourceAnalyzer(this.semanticModel, this.types); embeddedAnalyzer.Visit(node); return true; @@ -188,7 +205,7 @@ private void ExtractAttributes(BaseTypeDeclarationSyntax node) { foreach (var attribute in node.AttributeLists.SelectMany(a => a.Attributes)) { - var attributeDescription = new AttributeDescription(semanticModel.GetTypeDisplayString(attribute), attribute.Name.ToString()); + var attributeDescription = new AttributeDescription(this.semanticModel.GetTypeDisplayString(attribute), attribute.Name.ToString()); this.currentType.Attributes.Add(attributeDescription); if (attribute.ArgumentList != null) @@ -208,43 +225,33 @@ private void ExtractAttributes(BaseTypeDeclarationSyntax node) break; } - var argumentDescription = new AttributeArgumentDescription(argument.NameEquals?.Name.ToString() ?? argument.Expression?.ToString(), semanticModel.GetTypeDisplayString(argument.Expression), value); + var argumentDescription = new AttributeArgumentDescription(argument.NameEquals?.Name.ToString() ?? argument.Expression?.ToString(), this.semanticModel.GetTypeDisplayString(argument.Expression), value); attributeDescription.Arguments.Add(argumentDescription); } } } } - private string ExtractDocumentation(SyntaxNode node) + private DocumentationCommentsDescription ExtractDocumentation(SyntaxNode node) { - var documentationCommentXml = semanticModel.GetDeclaredSymbol(node)?.GetDocumentationCommentXml(); - - if (string.IsNullOrWhiteSpace(documentationCommentXml) || documentationCommentXml.StartsWith("